From 35ef9cd33defba82ad77c4d64f10c71a3eea7b3a Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 1 May 2017 14:37:00 -0500 Subject: [PATCH 0001/1359] Add Guild#nameAcronym and make avatar/iconURLs into functions (#1144) * Guild#iconURL(format, size); * OAuth2Application#iconURL(format, size); * User#iconURL(format, size); --- src/structures/Guild.js | 24 +++++++++++++---- src/structures/OAuth2Application.js | 22 +++++++++++----- src/structures/User.js | 16 +++++++---- src/util/Constants.js | 41 +++++++++++++++++++++++++---- 4 files changed, 82 insertions(+), 21 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index bbb74624f..ec0adb616 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -253,13 +253,27 @@ class Guild { } /** - * The URL to this guild's icon - * @type {?string} + * Gets the URL to this guild's icon + * @param {string} [format='webp'] One of `webp`, `png`, `jpg`, `gif` + * @param {number} [size=128] One of `128`, '256', `512`, `1024`, `2048` + * @returns {?string} + */ + iconURL(format, size) { + if (!this.icon) return null; + if (typeof format === 'number') { + size = format; + format = 'default'; + } + return Constants.Endpoints.Guild(this).Icon(this.client.options.http.cdn, this.icon, format, size); + } + + /** + * Gets the acronym that shows up in place of a guild icon + * @type {string} * @readonly */ - get iconURL() { - if (!this.icon) return null; - return Constants.Endpoints.Guild(this).Icon(this.client.options.http.cdn, this.icon); + get nameAcronym() { + return this.name.replace(/\w+/g, name => name[0]).replace(/\s/g, ''); } /** diff --git a/src/structures/OAuth2Application.js b/src/structures/OAuth2Application.js index 0e037ab7d..b6f64dd3b 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/OAuth2Application.js @@ -1,4 +1,5 @@ const Snowflake = require('../util/Snowflake'); +const Constants = require('../util/Constants'); /** * Represents an OAuth2 Application. @@ -41,12 +42,6 @@ class OAuth2Application { */ this.icon = data.icon; - /** - * The app's icon URL - * @type {string} - */ - this.iconURL = `https://cdn.discordapp.com/app-icons/${this.id}/${this.icon}.jpg`; - /** * The app's RPC origins * @type {?string[]} @@ -122,6 +117,21 @@ class OAuth2Application { return new Date(this.createdTimestamp); } + /** + * A link to the application's icon + * @param {string} [format='webp'] One of `webp`, `png`, `jpg`, `gif`. + * @param {number} [size=128] One of `128`, '256', `512`, `1024`, `2048` + * @returns {?string} URL to the icon + */ + iconURL(format, size) { + if (!this.icon) return null; + if (typeof format === 'number') { + size = format; + format = 'default'; + } + return Constants.Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.icon, format, size); + } + /** * Reset the app's secret and bot token. * @returns {OAuth2Application} diff --git a/src/structures/User.js b/src/structures/User.js index 4dbc0d84f..6c1042613 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -104,12 +104,18 @@ class User { /** * A link to the user's avatar - * @type {?string} - * @readonly + * @param {string} [format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, it will be `gif` + * for animated avatars or otherwise `webp` + * @param {number} [size=128] One of `128`, '256', `512`, `1024`, `2048` + * @returns {?string} avatarURL */ - get avatarURL() { + avatarURL(format, size) { if (!this.avatar) return null; - return Constants.Endpoints.User(this).Avatar(this.client.options.http.cdn, this.avatar); + if (typeof format === 'number') { + size = format; + format = 'default'; + } + return Constants.Endpoints.User(this).Avatar(this.client.options.http.cdn, this.avatar, format, size); } /** @@ -129,7 +135,7 @@ class User { * @readonly */ get displayAvatarURL() { - return this.avatarURL || this.defaultAvatarURL; + return this.avatarURL() || this.defaultAvatarURL; } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index f8895952b..b60977c22 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -91,6 +91,21 @@ exports.Errors = { INVALID_TOKEN: 'An invalid token was provided.', }; +const AllowedImageFormats = [ + 'webp', + 'png', + 'jpg', + 'gif', +]; + +const AllowedImageSizes = [ + 128, + 256, + 512, + 1024, + 2048, +]; + const Endpoints = exports.Endpoints = { User: userID => { if (userID.id) userID = userID.id; @@ -106,9 +121,9 @@ const Endpoints = exports.Endpoints = { Note: id => `${base}/notes/${id}`, Mentions: (limit, roles, everyone, guildID) => `${base}/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`, - Avatar: (root, hash) => { + Avatar: (root, hash, format, size) => { if (userID === '1') return hash; - return Endpoints.CDN(root).Avatar(userID, hash); + return Endpoints.CDN(root).Avatar(userID, hash, format, size); }, }; }, @@ -134,7 +149,7 @@ const Endpoints = exports.Endpoints = { settings: `${base}/settings`, auditLogs: `${base}/audit-logs`, Emoji: emojiID => Endpoints.CDN(root).Emoji(emojiID), - Icon: (root, hash) => Endpoints.CDN(root).Icon(guildID, hash), + Icon: (root, hash, format, size) => Endpoints.CDN(root).Icon(guildID, hash, format, size), Splash: (root, hash) => Endpoints.CDN(root).Splash(guildID, hash), Role: roleID => `${base}/roles/${roleID}`, Member: memberID => { @@ -190,8 +205,24 @@ const Endpoints = exports.Endpoints = { return { Emoji: emojiID => `${root}/emojis/${emojiID}.png`, Asset: name => `${root}/assets/${name}`, - Avatar: (userID, hash) => `${root}/avatars/${userID}/${hash}.${hash.startsWith('a_') ? 'gif' : 'png'}?size=2048`, - Icon: (guildID, hash) => `${root}/icons/${guildID}/${hash}.jpg`, + Avatar: (userID, hash, format = 'default', size) => { + if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; + if (!AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); + if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); + return `${root}/avatars/${userID}/${hash}.${format}${size ? `?size=${size}` : ''}`; + }, + Icon: (guildID, hash, format = 'default', size) => { + if (format === 'default') format = 'webp'; + if (!AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); + if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); + return `${root}/icons/${guildID}/${hash}.${format}${size ? `?size=${size}` : ''}`; + }, + AppIcon: (clientID, hash, format = 'default', size) => { + if (format === 'default') format = 'webp'; + if (!AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); + if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); + return `${root}/app-icons/${clientID}/${hash}.${format}${size ? `?size=${size}` : ''}`; + }, Splash: (guildID, hash) => `${root}/splashes/${guildID}/${hash}.jpg`, }; }, From e124ada962bfaf66fa263c3543461c1a6b9059da Mon Sep 17 00:00:00 2001 From: "Cody A. Taylor" Date: Mon, 1 May 2017 15:47:45 -0400 Subject: [PATCH 0002/1359] Document flattenErrors keys param (#1447) * Document flattenErrors keys param. * Remove parens. * Capitalise a letter --- src/client/rest/DiscordAPIError.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/rest/DiscordAPIError.js b/src/client/rest/DiscordAPIError.js index a28e3a8a5..84069bf03 100644 --- a/src/client/rest/DiscordAPIError.js +++ b/src/client/rest/DiscordAPIError.js @@ -18,13 +18,15 @@ class DiscordAPIError extends Error { /** * Flattens an errors object returned from the API into an array. * @param {Object} obj Discord errors object - * @param {string} [key] idklol + * @param {string} [key] Used internally to determine key names of nested fields * @returns {string[]} */ static flattenErrors(obj, key = '') { let messages = []; + for (const k of Object.keys(obj)) { const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k; + if (obj[k]._errors) { messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`); } else { From 7da53af0c383b080b0997e5507cb8e870821b446 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 1 May 2017 21:07:46 +0100 Subject: [PATCH 0003/1359] Improve GuildAuditLogs documentation by creating an AuditLogChange typedef --- src/structures/GuildAuditLogs.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 6f6e04dcd..3e32cdb86 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -165,9 +165,17 @@ class GuildAuditLogsEntry { */ this.executor = guild.client.users.get(data.user_id); + /** + * An entry in the audit log representing a specific change + * @typedef {object} AuditLogChange + * @property {string} name The name of the change, e.g. `nick` for nickname changes + * @property {string|boolean|number} [old] The old value of the change, e.g. for nicknames, the old nickname + * @property {string|boolean|number} new The new value of the change, e.g. for nicknames, the new nickname + */ + /** * Specific property changes - * @type {Object[]} + * @type {AuditLogChange[]} */ this.changes = data.changes ? data.changes.map(c => ({ name: c.key, old: c.old_value, new: c.new_value })) : null; From b7a81ed7e1dfbff2f46e66a09353311247a125a4 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 1 May 2017 22:07:20 +0100 Subject: [PATCH 0004/1359] watch me, gus --- src/structures/GuildAuditLogs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 3e32cdb86..0b2a464be 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -168,7 +168,7 @@ class GuildAuditLogsEntry { /** * An entry in the audit log representing a specific change * @typedef {object} AuditLogChange - * @property {string} name The name of the change, e.g. `nick` for nickname changes + * @property {string} key The property that was changed, e.g. `nick` for nickname changes * @property {string|boolean|number} [old] The old value of the change, e.g. for nicknames, the old nickname * @property {string|boolean|number} new The new value of the change, e.g. for nicknames, the new nickname */ @@ -177,7 +177,7 @@ class GuildAuditLogsEntry { * Specific property changes * @type {AuditLogChange[]} */ - this.changes = data.changes ? data.changes.map(c => ({ name: c.key, old: c.old_value, new: c.new_value })) : null; + this.changes = data.changes ? data.changes.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) : null; /** * The ID of this entry From a7c902c6cfbd0a583f40d983a9f0e5d20cfa6e5d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 1 May 2017 22:10:45 +0100 Subject: [PATCH 0005/1359] New is also optional --- src/structures/GuildAuditLogs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 0b2a464be..1e1f70b8b 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -170,7 +170,7 @@ class GuildAuditLogsEntry { * @typedef {object} AuditLogChange * @property {string} key The property that was changed, e.g. `nick` for nickname changes * @property {string|boolean|number} [old] The old value of the change, e.g. for nicknames, the old nickname - * @property {string|boolean|number} new The new value of the change, e.g. for nicknames, the new nickname + * @property {string|boolean|number} [new] The new value of the change, e.g. for nicknames, the new nickname */ /** From 07623d49e251b462a2e2f22cae31d3c8dfcb090d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 2 May 2017 07:58:42 +0200 Subject: [PATCH 0006/1359] Fix for sending files with Webhook class (#1449) --- src/structures/Webhook.js | 47 +++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 4d96382ac..509de2e66 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -81,6 +81,7 @@ class Webhook { * @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 {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if * it exceeds the character limit. If an object is provided, these are the options for splitting the message. @@ -88,7 +89,7 @@ class Webhook { /** * Send a message with this webhook. - * @param {StringResolvable} content The content to send + * @param {StringResolvable} [content] The content to send * @param {WebhookMessageOptions} [options={}] The options to provide * @returns {Promise} * @example @@ -104,24 +105,36 @@ class Webhook { } 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, - }) - ); + if (options.files) options.files.push(options.file); + else options.files = [options.file]; } + + if (options.files) { + for (const i in options.files) { + let file = options.files[i]; + if (typeof file === 'string') file = { attachment: file }; + if (!file.name) { + if (typeof file.attachment === 'string') { + file.name = path.basename(file.attachment); + } else if (file.attachment && file.attachment.path) { + file.name = path.basename(file.attachment.path); + } else { + file.name = 'file.jpg'; + } + } + options.files[i] = file; + } + + return Promise.all(options.files.map(file => + this.client.resolver.resolveBuffer(file.attachment).then(buffer => { + file.file = buffer; + return file; + }) + )).then(files => this.client.rest.methods.sendWebhookMessage(this, content, options, files)); + } + return this.client.rest.methods.sendWebhookMessage(this, content, options); } From 3d92c6d3169e6ecc963bcec6009035301e7ae272 Mon Sep 17 00:00:00 2001 From: Anxeal Date: Wed, 3 May 2017 21:45:51 +0300 Subject: [PATCH 0007/1359] Fix typo in RESTMethods.js (#1454) fetchMeMentions -> fetchMentions :thinking: --- src/client/rest/RESTMethods.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 69d29ecba..3ab87a8bd 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -763,7 +763,7 @@ class RESTMethods { ); } - fetchMeMentions(options) { + fetchMentions(options) { if (options.guild) options.guild = options.guild.id ? options.guild.id : options.guild; return this.rest.makeRequest( 'get', From ac5d4d1050e6182eed29c3d6efbf8844c83edb54 Mon Sep 17 00:00:00 2001 From: Anxeal Date: Wed, 3 May 2017 22:35:24 +0300 Subject: [PATCH 0008/1359] Fix typo in RESTMethods.js (#1455) Mentions should be written with a capital M --- src/client/rest/RESTMethods.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 3ab87a8bd..aa3d0fe8b 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -767,7 +767,7 @@ class RESTMethods { if (options.guild) options.guild = options.guild.id ? options.guild.id : options.guild; return this.rest.makeRequest( 'get', - Endpoints.User('@me').mentions(options.limit, options.roles, options.everyone, options.guild) + Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild) ).then(res => res.body.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); } From caf96339e02b22ecc60776fec09e75534779a7ac Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 3 May 2017 17:50:08 -0500 Subject: [PATCH 0009/1359] Update GuildAuditLogs.js (#1456) --- src/structures/GuildAuditLogs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 1e1f70b8b..3c3a0b7ee 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -37,6 +37,7 @@ const Actions = { EMOJI_CREATE: 60, EMOJI_UPDATE: 61, EMOJI_DELETE: 62, + MESSAGE_DELETE: 72, }; @@ -112,6 +113,7 @@ class GuildAuditLogs { Actions.INVITE_DELETE, Actions.WEBHOOK_DELETE, Actions.EMOJI_DELETE, + Actions.MESSAGE_DELETE, ].includes(action)) return 'DELETE'; if ([ From 8c6167d35a84d1d81c1b548e89b83cecb1cb5397 Mon Sep 17 00:00:00 2001 From: Crawl Date: Fri, 5 May 2017 01:56:00 +0200 Subject: [PATCH 0010/1359] Update welcome docs page --- 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 5a11c91e5..01a3d6717 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -80,7 +80,7 @@ client.login('your token'); ## Contributing Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the [documentation](https://discord.js.org/#/docs). -See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/CONTRIBUTING.md) if you'd like to submit a PR. +See [the contribution guide](https://github.com/hydrabolt/discord.js/blob/master/.github/CONTRIBUTING.md) if you'd like to submit a PR. ## Help If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle From e6437388cfec19ab5804839b64504e379e9bb9a6 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 May 2017 02:22:55 +0200 Subject: [PATCH 0011/1359] Endpoints.Guild(...).Emoji(...) should not use CDN (#1462) --- src/util/Constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index b60977c22..1a4efcca2 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -148,7 +148,7 @@ const Endpoints = exports.Endpoints = { ack: `${base}/ack`, settings: `${base}/settings`, auditLogs: `${base}/audit-logs`, - Emoji: emojiID => Endpoints.CDN(root).Emoji(emojiID), + Emoji: emojiID => `${base}/emojis/${emojiID}`, Icon: (root, hash, format, size) => Endpoints.CDN(root).Icon(guildID, hash, format, size), Splash: (root, hash) => Endpoints.CDN(root).Splash(guildID, hash), Role: roleID => `${base}/roles/${roleID}`, From 2a23941020bd60876a2d4efc0f1004a9a373a445 Mon Sep 17 00:00:00 2001 From: Crawl Date: Fri, 5 May 2017 17:08:41 +0200 Subject: [PATCH 0012/1359] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e0d48d083..7d5ef3c8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord.js", - "version": "11.1.0", + "version": "12.0.0", "description": "A powerful library for interacting with the Discord API", "main": "./src/index", "types": "./typings/index.d.ts", From 128b4ee3e8e1aeb1fc4303e4b33888bf98ac3724 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 May 2017 19:44:54 +0200 Subject: [PATCH 0013/1359] update GuildAuditLogs for MESSAGE_DELETE and fixed extras (#1464) * update GuildAuditLogs for MESSAGE_DELETE and fixed extras * correct oder of targets --- src/structures/GuildAuditLogs.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 3c3a0b7ee..ee1ec5156 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -9,6 +9,7 @@ const Targets = { INVITE: 'INVITE', WEBHOOK: 'WEBHOOK', EMOJI: 'EMOJI', + MESSAGE: 'MESSAGE', }; const Actions = { @@ -83,6 +84,7 @@ class GuildAuditLogs { if (target < 50) return Targets.INVITE; if (target < 60) return Targets.WEBHOOK; if (target < 70) return Targets.EMOJI; + if (target < 80) return Targets.MESSAGE; return null; } @@ -198,15 +200,20 @@ class GuildAuditLogsEntry { removed: data.options.members_removed, days: data.options.delete_member_days, }; + } else if (data.action_type === Actions.MESSAGE_DELETE) { + this.extra = { + count: data.options.count, + channel: guild.channels.get(data.options.channel_id), + }; } else { switch (data.options.type) { case 'member': - this.extra = guild.members.get(this.options.id); - if (!this.extra) this.extra = { id: this.options.id }; + this.extra = guild.members.get(data.options.id); + if (!this.extra) this.extra = { id: data.options.id }; break; case 'role': - this.extra = guild.roles.get(this.options.id); - if (!this.extra) this.extra = { id: this.options.id, name: this.options.role_name }; + this.extra = guild.roles.get(data.options.id); + if (!this.extra) this.extra = { id: data.options.id, name: data.options.role_name }; break; default: break; @@ -233,6 +240,8 @@ class GuildAuditLogsEntry { this.target = invites.find(i => i.code === (change.new || change.old)); return this.target; }); + } else if (targetType === Targets.MESSAGE) { + this.target = guild.client.users.get(data.target_id); } else { this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id); } From a4e57137904cc79a198aa2f3973db206d320adf1 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 May 2017 20:15:06 +0200 Subject: [PATCH 0014/1359] readded docs for Client#error and Client#ready (#1466) --- src/client/websocket/WebSocketConnection.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index ecbcf141a..b102e05f5 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -115,6 +115,10 @@ class WebSocketConnection extends EventEmitter { this.debug('Tried to mark self as ready, but already ready'); return; } + /** + * Emitted when the client becomes ready to start working. + * @event Client#ready + */ this.status = Constants.Status.READY; this.client.emit(Constants.Events.READY); this.packetManager.handleQueue(); @@ -354,6 +358,11 @@ class WebSocketConnection extends EventEmitter { * @param {Error} error Error that occurred */ onError(error) { + /** + * Emitted whenever the client's WebSocket encounters a connection error. + * @event Client#error + * @param {Error} error The encountered error + */ this.client.emit(Constants.Events.ERROR, error); if (error.message === 'uWs client connection error') this.reconnect(); } From a685d245042b89dff8d7dd667bc11b1cc7cfb15c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 May 2017 23:19:07 +0200 Subject: [PATCH 0015/1359] using correct properties for invites (#1467) --- src/structures/GuildAuditLogs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index ee1ec5156..e53126edd 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -234,10 +234,10 @@ class GuildAuditLogsEntry { return this.target; }); } else if (targetType === Targets.INVITE) { - const change = this.changes.find(c => c.name === 'code'); + const change = this.changes.find(c => c.key === 'code'); this.target = guild.fetchInvites() .then(invites => { - this.target = invites.find(i => i.code === (change.new || change.old)); + this.target = invites.find(i => i.code === (change.new_value || change.old_value)); return this.target; }); } else if (targetType === Targets.MESSAGE) { From d9e5bdea19b2d2f2e3f75c0ffd0bfd8e717b1ae6 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 May 2017 23:24:02 +0200 Subject: [PATCH 0016/1359] added Invite#presenceCount and Invite#memberCount (#1460) * added Invite#online and Invite#memberCount * requested change --- src/structures/Invite.js | 12 ++++++++++++ src/util/Constants.js | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 8a7b962cd..995f9c57e 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -54,6 +54,12 @@ class Invite { */ this.code = data.code; + /** + * The approximate number of online members of the guild this invite is for + * @type {number} + */ + this.presenceCount = data.approximate_presence_count; + /** * Whether or not this invite is temporary * @type {boolean} @@ -66,6 +72,12 @@ class Invite { */ this.maxAge = data.max_age; + /** + * The approximate total number of members of the guild this invite is for + * @type {number} + */ + this.memberCount = data.approximate_member_count; + /** * How many times this invite has been used * @type {number} diff --git a/src/util/Constants.js b/src/util/Constants.js index 1a4efcca2..f2018e970 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -243,7 +243,7 @@ const Endpoints = exports.Endpoints = { toString: () => '/gateway', bot: '/gateway/bot', }, - Invite: inviteID => `/invite/${inviteID}`, + Invite: inviteID => `/invite/${inviteID}?with_counts=true`, inviteLink: id => `https://discord.gg/${id}`, Webhook: (webhookID, token) => `/webhooks/${webhookID}${token ? `/${token}` : ''}`, }; From e66fa145d2b0b31254be513bbf4e43e896198f15 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 6 May 2017 01:04:12 +0200 Subject: [PATCH 0017/1359] fix fetchMentions' auth header, options and data mapping (#1457) * fix fetchMentions' auth header, options and data mapping * vscode strikes again * switched to Util.mergeDefault * vscode * removed duplicated optionals and switched to instanceof --- src/client/rest/RESTMethods.js | 9 +++++---- src/structures/ClientUser.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index aa3d0fe8b..7c9030ad4 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -764,11 +764,12 @@ class RESTMethods { } fetchMentions(options) { - if (options.guild) options.guild = options.guild.id ? options.guild.id : options.guild; + if (options.guild instanceof Guild) options.guild = options.guild.id; + Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); + return this.rest.makeRequest( - 'get', - Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild) - ).then(res => res.body.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); + 'get', Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild), true + ).then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); } addFriend(user) { diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 9fe1a3d96..dcd16423c 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -265,7 +265,7 @@ class ClientUser extends User { * @param {Guild|Snowflake} [options.guild] Limit the search to a specific guild * @returns {Promise} */ - fetchMentions(options = { limit: 25, roles: true, everyone: true, guild: null }) { + fetchMentions(options = {}) { return this.client.rest.methods.fetchMentions(options); } From 5bfc688bb04940fe1133252d5342e320dd3bf445 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 6 May 2017 01:09:01 +0200 Subject: [PATCH 0018/1359] Using a traditional for loop rather than a for in loop for options.files (#1461) --- src/structures/Webhook.js | 2 +- src/structures/interfaces/TextBasedChannel.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 509de2e66..bf997008e 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -112,7 +112,7 @@ class Webhook { } if (options.files) { - for (const i in options.files) { + for (let i = 0; i < options.files.length; i++) { let file = options.files[i]; if (typeof file === 'string') file = { attachment: file }; if (!file.name) { diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index f7fde7319..8fb7995ec 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -88,7 +88,7 @@ class TextBasedChannel { } if (options.files) { - for (const i in options.files) { + for (let i = 0; i < options.files.length; i++) { let file = options.files[i]; if (typeof file === 'string') file = { attachment: file }; if (!file.name) { From 6566c0d3c5ecf7a0eab1739b2b98d03fa1ac31b7 Mon Sep 17 00:00:00 2001 From: 1Computer1 <1Computer1@users.noreply.github.com> Date: Fri, 5 May 2017 19:22:15 -0400 Subject: [PATCH 0019/1359] Deprecate aliases (#1469) --- src/structures/Message.js | 1 + src/structures/Webhook.js | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/structures/Message.js b/src/structures/Message.js index dfd5fdd2e..4c318ce73 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -388,6 +388,7 @@ class Message { * @param {string} lang The language for the code block * @param {StringResolvable} content The new content for the message * @returns {Promise} + * @deprecated */ editCode(lang, content) { content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index bf997008e..1f66ec64f 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -143,6 +143,7 @@ class Webhook { * @param {StringResolvable} content The content to send * @param {WebhookMessageOptions} [options={}] The options to provide * @returns {Promise} + * @deprecated * @example * // Send a message * webhook.sendMessage('hello!') @@ -160,6 +161,7 @@ class Webhook { * @param {StringResolvable} [content] Text message to send with the attachment * @param {WebhookMessageOptions} [options] The options to provide * @returns {Promise} + * @deprecated */ sendFile(attachment, name, content, options = {}) { return this.send(content, Object.assign(options, { file: { attachment, name } })); @@ -171,6 +173,7 @@ class Webhook { * @param {StringResolvable} content Content of the code block * @param {WebhookMessageOptions} options The options to provide * @returns {Promise} + * @deprecated */ sendCode(lang, content, options = {}) { return this.send(content, Object.assign(options, { code: lang })); From f95f18b586069bb40ebe027c4a014a0263190641 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 9 May 2017 00:19:24 +0200 Subject: [PATCH 0020/1359] Handing promise rejections from GuildAuditLogs#build to the user (#1474) * handing guildauditlog's promise rejections to the user * Returning a new Promise to resolve a Promise.all is unnecessary. Also for the docs, it returns a Promise, not GuildAuditLogs directly. * totally did not removed that line --- src/structures/GuildAuditLogs.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index e53126edd..9202ed231 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -62,13 +62,11 @@ class GuildAuditLogs { /** * Handles possible promises for entry targets. - * @returns {GuildAuditLogs} + * @returns {Promise} */ static build(...args) { - return new Promise(resolve => { - const logs = new GuildAuditLogs(...args); - Promise.all(logs.entries.map(e => e.target)).then(() => resolve(logs)); - }); + const logs = new GuildAuditLogs(...args); + return Promise.all(logs.entries.map(e => e.target)).then(() => logs); } /** From 83f2c62c3f383814c32d2cbe6526c06a5e6b1e1f Mon Sep 17 00:00:00 2001 From: meew0 Date: Wed, 10 May 2017 13:11:21 +0200 Subject: [PATCH 0021/1359] Fix the mention example in the USERS_PATTERN doc comment Previously it was a channel mention. Thanks @Hackzzila. --- src/structures/MessageMentions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index b045d5a70..0e8f6de83 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -124,7 +124,7 @@ class MessageMentions { MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; /** - * Regular expression that globally matches user mentions like `<#81440962496172032>` + * Regular expression that globally matches user mentions like `<@81440962496172032>` * @type {RegExp} */ MessageMentions.USERS_PATTERN = /<@!?[0-9]+>/g; From d11a658f40d572118e7cd443b8d2fcfd2fef9a37 Mon Sep 17 00:00:00 2001 From: Drahcirius Date: Wed, 10 May 2017 11:14:39 -0400 Subject: [PATCH 0022/1359] invalid token errors not rejected properly (#1478) * ready event will now throw errors properly * ws login rejection fix --- src/client/ClientManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 551e20dc1..538411079 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -43,7 +43,7 @@ class ClientManager { const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`; this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`); this.client.ws.connect(gateway); - this.client.ws.once('close', event => { + this.client.ws.connection.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)); From ff8229707388215a4110525db99a727d46135627 Mon Sep 17 00:00:00 2001 From: aemino Date: Thu, 11 May 2017 21:41:40 -0700 Subject: [PATCH 0023/1359] GuildMember#setVoiceChannel fix (#1482) Looks like someone forgot to remove the full channel object from the PATCH payload. --- src/client/rest/RESTMethods.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 7c9030ad4..30d5f4ff9 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -465,7 +465,10 @@ class RESTMethods { } updateGuildMember(member, data) { - if (data.channel) data.channel_id = this.client.resolver.resolveChannel(data.channel).id; + if (data.channel) { + data.channel_id = this.client.resolver.resolveChannel(data.channel).id; + data.channel = null; + } if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); let endpoint = Endpoints.Member(member); From ff3e602134601a0d5e729a4c117f52c039eedfef Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 12 May 2017 15:30:46 +0200 Subject: [PATCH 0024/1359] Failing to resolve a role should reject and not throw an error (#1483) --- src/structures/GuildMember.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index bcad0e03e..0c096120d 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -394,7 +394,7 @@ class GuildMember { */ addRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); - if (!role) throw new TypeError('Supplied parameter was neither a Role nor a Snowflake.'); + if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); return this.client.rest.methods.addMemberRole(this, role); } @@ -421,7 +421,7 @@ class GuildMember { */ removeRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); - if (!role) throw new TypeError('Supplied parameter was neither a Role nor a Snowflake.'); + if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); return this.client.rest.methods.removeMemberRole(this, role); } From 6ac2252794b7057f3343fb54481a379265801441 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 13 May 2017 10:41:32 -0500 Subject: [PATCH 0025/1359] make default avatar resolving better (#1484) * Update Constants.js * Update User.js * Update Constants.js * Update Constants.js --- src/structures/User.js | 4 +--- src/util/Constants.js | 9 +-------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/structures/User.js b/src/structures/User.js index 6c1042613..ae75778a9 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -124,9 +124,7 @@ class User { * @readonly */ get defaultAvatarURL() { - const avatars = Object.keys(Constants.DefaultAvatars); - const avatar = avatars[this.discriminator % avatars.length]; - return Constants.Endpoints.CDN(this.client.options.http.host).Asset(`${Constants.DefaultAvatars[avatar]}.png`); + return Constants.Endpoints.CDN(this.client.options.http.host).DefaultAvatar(this.discriminator % 5); } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index f2018e970..3678410ac 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -205,6 +205,7 @@ const Endpoints = exports.Endpoints = { return { Emoji: emojiID => `${root}/emojis/${emojiID}.png`, Asset: name => `${root}/assets/${name}`, + DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, Avatar: (userID, hash, format = 'default', size) => { if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; if (!AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); @@ -451,14 +452,6 @@ exports.MessageTypes = [ 'GUILD_MEMBER_JOIN', ]; -exports.DefaultAvatars = { - BLURPLE: '6debd47ed13483642cf09e832ed0bc1b', - GREY: '322c936a8c8be1b803cd94861bdfa868', - GREEN: 'dd4dbc0016779df1378e7812eabaa04d', - ORANGE: '0e291f67c9274a1abdddeb3fd919cbaa', - RED: '1cbd08c76f8af6dddce02c5138971129', -}; - exports.ExplicitContentFilterTypes = [ 'DISABLED', 'NON_FRIENDS', From b0d4b53d6b4c7b278cefe92573deb1250ed759fa Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 13 May 2017 18:17:25 +0200 Subject: [PATCH 0026/1359] Fix webpack uglify --- package.json | 3 ++- webpack.config.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7d5ef3c8d..0d1d82822 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "discord.js-docgen": "hydrabolt/discord.js-docgen", "eslint": "^3.19.0", "parallel-webpack": "^1.6.0", - "uglify-js": "mishoo/UglifyJS2#harmony", + "uglify-js": "mishoo/UglifyJS2#harmony-v2.8.22", + "uglifyjs-webpack-plugin": "^0.4.3", "webpack": "^2.2.0" }, "engines": { diff --git a/webpack.config.js b/webpack.config.js index 73e6c43ec..dd4000fd9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,6 +5,7 @@ const webpack = require('webpack'); const createVariants = require('parallel-webpack').createVariants; +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const version = require('./package.json').version; const createConfig = options => { @@ -12,7 +13,7 @@ const createConfig = options => { new webpack.DefinePlugin({ 'global.GENTLY': false }), ]; - if (options.minify) plugins.push(new webpack.optimize.UglifyJsPlugin({ minimize: true })); + if (options.minify) plugins.push(new UglifyJSPlugin({ minimize: true })); const filename = `./webpack/discord${process.env.VERSIONED === 'false' ? '' : '.' + version}${options.minify ? '.min' : ''}.js`; // eslint-disable-line From eab5c9e7f185318b5932a8703004d1eb08f10214 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 13 May 2017 18:42:15 +0200 Subject: [PATCH 0027/1359] Add tarvis staging --- .travis.yml | 36 ++++++++++++++++++++---------------- deploy/deploy.sh | 19 ++++++------------- deploy/test.sh | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 deploy/test.sh diff --git a/.travis.yml b/.travis.yml index e1f264647..82dcb3e03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,20 @@ -language: node_js -node_js: - - "6" - - "7" -cache: - directories: - - node_modules -install: npm install -script: - - bash ./deploy/deploy.sh -env: - global: - - ENCRYPTION_LABEL: "af862fa96d3e" - - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" -dist: trusty -sudo: false +language: node_js +node_js: + - "6" + - "7" +cache: + directories: + - node_modules +install: npm install +jobs: + include: + - stage: test + script: bash ./deploy/test.sh + - stage: build + script: bash ./deploy/deploy.sh +env: + global: + - ENCRYPTION_LABEL: "af862fa96d3e" + - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" +dist: trusty +sudo: false diff --git a/deploy/deploy.sh b/deploy/deploy.sh index c132a20f6..e63a81b82 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -3,13 +3,6 @@ set -e -function tests { - npm run lint - npm run docs:test - VERSIONED=false npm run webpack - exit 0 -} - function build { npm run lint npm run docs @@ -22,10 +15,10 @@ if [[ "$TRAVIS_BRANCH" == revert-* ]]; then exit 0 fi -# For PRs, only run tests +# For PRs, do nothing if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running tests." - tests + echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - doing nothing." + exit 0 fi # Figure out the source of the build @@ -39,10 +32,10 @@ else SOURCE_TYPE="branch" fi -# For Node != 6, only run tests +# For Node != 6, do nothing if [ "$TRAVIS_NODE_VERSION" != "6" ]; then - echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - only running tests." - tests + echo -e "\e[36m\e[1mBuild triggered with Node v${TRAVIS_NODE_VERSION} - doing nothing." + exit 0 fi build diff --git a/deploy/test.sh b/deploy/test.sh new file mode 100644 index 000000000..7c356ae9f --- /dev/null +++ b/deploy/test.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +function tests { + npm run lint + npm run docs:test + VERSIONED=false npm run webpack + exit 0 +} + +# For revert branches, do nothing +if [[ "$TRAVIS_BRANCH" == revert-* ]]; then + echo -e "\e[36m\e[1mTest triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing." + exit 0 +fi + +# For PRs +if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo -e "\e[36m\e[1mTest triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running tests." + tests +fi + +# Figure out the source of the test +if [ -n "$TRAVIS_TAG" ]; then + echo -e "\e[36m\e[1mTest triggered for tag \"${TRAVIS_TAG}\"." +else + echo -e "\e[36m\e[1mTest triggered for branch \"${TRAVIS_BRANCH}\"." +fi + +# For Node != 6 +if [ "$TRAVIS_NODE_VERSION" != "6" ]; then + echo -e "\e[36m\e[1mTest triggered with Node v${TRAVIS_NODE_VERSION} - only running tests." + tests +fi From b4645685aa77d9d1fbfc2c0e6fde70c4e0dd46af Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 13 May 2017 18:58:56 +0200 Subject: [PATCH 0028/1359] Improve test and build scripts --- .travis.yml | 1 + deploy/deploy.sh | 1 - deploy/test.sh | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 82dcb3e03..88e0a9d4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ jobs: - stage: test script: bash ./deploy/test.sh - stage: build + node_js: "6" script: bash ./deploy/deploy.sh env: global: diff --git a/deploy/deploy.sh b/deploy/deploy.sh index e63a81b82..d12ee608b 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -4,7 +4,6 @@ set -e function build { - npm run lint npm run docs VERSIONED=false npm run webpack } diff --git a/deploy/test.sh b/deploy/test.sh index 7c356ae9f..9e076a4c4 100644 --- a/deploy/test.sh +++ b/deploy/test.sh @@ -5,7 +5,6 @@ set -e function tests { npm run lint npm run docs:test - VERSIONED=false npm run webpack exit 0 } From 833bfa1cbaf0cbde8ba61efb46c41008ed249281 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 13 May 2017 19:04:30 +0200 Subject: [PATCH 0029/1359] Move node versions --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 88e0a9d4a..24ba2bf1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,4 @@ language: node_js -node_js: - - "6" - - "7" cache: directories: - node_modules @@ -9,6 +6,9 @@ install: npm install jobs: include: - stage: test + node_js: + - "6" + - "7" script: bash ./deploy/test.sh - stage: build node_js: "6" From 5db3ab742b230d308c7aee72004d40bbbdbb155f Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 13 May 2017 19:09:26 +0200 Subject: [PATCH 0030/1359] Update travis build stages --- .travis.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 24ba2bf1e..74323ddbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,14 @@ language: node_js +node_js: + - "6" + - "7" cache: directories: - node_modules install: npm install +script: bash ./deploy/test.sh jobs: include: - - stage: test - node_js: - - "6" - - "7" - script: bash ./deploy/test.sh - stage: build node_js: "6" script: bash ./deploy/deploy.sh From 342c644043645e62ef949007eac4e11d53f3c532 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sun, 14 May 2017 19:30:23 +0200 Subject: [PATCH 0031/1359] Improve avatar example --- docs/examples/avatars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/avatars.js b/docs/examples/avatars.js index c95337705..cf59a6c94 100644 --- a/docs/examples/avatars.js +++ b/docs/examples/avatars.js @@ -22,7 +22,7 @@ client.on('message', message => { // If the message is "what is my avatar" if (message.content === 'what is my avatar') { // Send the user's avatar URL - message.reply(message.author.avatarURL); + message.reply(message.author.displayAvatarURL); } }); From 4422f2aa8a72ae8765d91c2b1847785992075c33 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 14 May 2017 12:33:04 -0500 Subject: [PATCH 0032/1359] fix infinte loop issue (#1488) --- src/client/rest/DiscordAPIError.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/rest/DiscordAPIError.js b/src/client/rest/DiscordAPIError.js index 84069bf03..7d3ff7ef9 100644 --- a/src/client/rest/DiscordAPIError.js +++ b/src/client/rest/DiscordAPIError.js @@ -29,6 +29,8 @@ class DiscordAPIError extends Error { if (obj[k]._errors) { messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`); + } else if (obj[k].code && obj[k].message) { + messages.push(`${obj[k].code}: ${obj[k].message}`); } else { messages = messages.concat(this.flattenErrors(obj[k], newKey)); } From ca926ee40414f6a0f022803e8cf75ab1e23628db Mon Sep 17 00:00:00 2001 From: Crawl Date: Sun, 14 May 2017 20:15:55 +0200 Subject: [PATCH 0033/1359] Remove all deprecated methods / props --- src/structures/DMChannel.js | 5 - src/structures/GroupDMChannel.js | 5 - src/structures/GuildMember.js | 20 ---- src/structures/Message.js | 12 --- src/structures/MessageCollector.js | 21 ---- src/structures/Role.js | 16 ---- src/structures/TextChannel.js | 5 - src/structures/User.js | 4 - src/structures/Webhook.js | 43 +-------- src/structures/interfaces/TextBasedChannel.js | 94 +----------------- src/util/Permissions.js | 95 +------------------ 11 files changed, 3 insertions(+), 317 deletions(-) diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 9c74aad4e..0f61f773f 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -39,11 +39,6 @@ class DMChannel extends Channel { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} - sendMessage() {} - sendEmbed() {} - sendFile() {} - sendFiles() {} - sendCode() {} fetchMessage() {} fetchMessages() {} fetchPinnedMessages() {} diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index d7ee76b50..30a11fc5c 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -155,11 +155,6 @@ class GroupDMChannel extends Channel { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} - sendMessage() {} - sendEmbed() {} - sendFile() {} - sendFiles() {} - sendCode() {} fetchMessage() {} fetchMessages() {} fetchPinnedMessages() {} diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 0c096120d..49de7a1b2 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -3,7 +3,6 @@ const Role = require('./Role'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); const Presence = require('./Presence').Presence; -const util = require('util'); /** * Represents a member of a guild on Discord. @@ -310,18 +309,6 @@ class GuildMember { return this.roles.some(r => r.hasPermission(permission, undefined, checkAdmin)); } - /** - * Checks whether the roles of the member allows them to perform specific actions. - * @param {PermissionResolvable[]} permissions The permissions to check for - * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions - * @returns {boolean} - * @deprecated - */ - hasPermissions(permissions, explicit = false) { - if (!explicit && this.user.id === this.guild.ownerID) return true; - return this.hasPermission(permissions, explicit); - } - /** * Checks whether the roles of the member allows them to perform specific actions, and lists any missing permissions. * @param {PermissionResolvable[]} permissions The permissions to check for @@ -509,15 +496,8 @@ class GuildMember { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} - sendMessage() {} - sendEmbed() {} - sendFile() {} - sendCode() {} } TextBasedChannel.applyToClass(GuildMember); -GuildMember.prototype.hasPermissions = util.deprecate(GuildMember.prototype.hasPermissions, - 'GuildMember#hasPermissions is deprecated - use GuildMember#hasPermission, it now takes an array'); - module.exports = GuildMember; diff --git a/src/structures/Message.js b/src/structures/Message.js index 4c318ce73..8ca7b75a1 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -383,18 +383,6 @@ class Message { return this.client.rest.methods.updateMessage(this, content, options); } - /** - * Edit the content of the message, with a code block. - * @param {string} lang The language for the code block - * @param {StringResolvable} content The new content for the message - * @returns {Promise} - * @deprecated - */ - editCode(lang, content) { - content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); - return this.edit(`\`\`\`${lang || ''}\n${content}\n\`\`\``); - } - /** * Pins this message to the channel's pinned messages. * @returns {Promise} diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index fd5bebda4..35a741133 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -1,5 +1,4 @@ const Collector = require('./interfaces/Collector'); -const util = require('util'); /** * @typedef {CollectorOptions} MessageCollectorOptions @@ -35,29 +34,9 @@ class MessageCollector extends Collector { this.client.on('message', this.listener); - // For backwards compatibility (remove in v12) - if (this.options.max) this.options.maxProcessed = this.options.max; - if (this.options.maxMatches) this.options.max = this.options.maxMatches; - this._reEmitter = message => { - /** - * Emitted when the collector receives a message. - * @event MessageCollector#message - * @param {Message} message The message - * @deprecated - */ - this.emit('message', message); - }; this.on('collect', this._reEmitter); } - // Remove in v12 - on(eventName, listener) { - if (eventName === 'message') { - listener = util.deprecate(listener, 'MessageCollector will soon no longer emit "message", use "collect" instead'); - } - super.on(eventName, listener); - } - /** * Handle an incoming message for possible collection. * @param {Message} message The message that could be collected diff --git a/src/structures/Role.js b/src/structures/Role.js index d5ffb33b1..22594fd8c 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -1,6 +1,5 @@ const Snowflake = require('../util/Snowflake'); const Permissions = require('../util/Permissions'); -const util = require('util'); /** * Represents a role on Discord. @@ -167,17 +166,6 @@ class Role { ); } - /** - * Checks if the role has all specified permissions. - * @param {PermissionResolvable[]} permissions The permissions to check for - * @param {boolean} [explicit=false] Whether to require the role to explicitly have the exact permissions - * @returns {boolean} - * @deprecated - */ - hasPermissions(permissions, explicit = false) { - return new Permissions(this.permissions).has(permissions, !explicit); - } - /** * Compares this role's position to another role's. * @param {Role} role Role to compare to this one @@ -351,8 +339,4 @@ class Role { } } -Role.prototype.hasPermissions = util - .deprecate(Role.prototype.hasPermissions, - 'Role#hasPermissions is deprecated - use Role#hasPermission instead, it now takes an array'); - module.exports = Role; diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 20b3c54f3..818b9217a 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -84,11 +84,6 @@ class TextChannel extends GuildChannel { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} - sendMessage() {} - sendEmbed() {} - sendFile() {} - sendFiles() {} - sendCode() {} fetchMessage() {} fetchMessages() {} fetchPinnedMessages() {} diff --git a/src/structures/User.js b/src/structures/User.js index ae75778a9..b7b6eb96d 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -296,10 +296,6 @@ class User { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} - sendMessage() {} - sendEmbed() {} - sendFile() {} - sendCode() {} } TextBasedChannel.applyToClass(User); diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 1f66ec64f..ca20b0943 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -70,7 +70,7 @@ class Webhook { } /** - * Options that can be passed into send, sendMessage, sendFile, sendEmbed, and sendCode. + * Options that can be passed into send. * @typedef {Object} WebhookMessageOptions * @property {string} [username=this.name] Username override for the message * @property {string} [avatarURL] Avatar URL override for the message @@ -138,47 +138,6 @@ class Webhook { return this.client.rest.methods.sendWebhookMessage(this, content, options); } - /** - * Send a message with this webhook - * @param {StringResolvable} content The content to send - * @param {WebhookMessageOptions} [options={}] The options to provide - * @returns {Promise} - * @deprecated - * @example - * // Send a message - * webhook.sendMessage('hello!') - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); - */ - sendMessage(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} - * @deprecated - */ - 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} - * @deprecated - */ - sendCode(lang, content, options = {}) { - return this.send(content, Object.assign(options, { code: lang })); - } - /** * Send a raw slack message with this webhook. * @param {Object} body The raw body to send diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 8fb7995ec..226f67b65 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -2,7 +2,6 @@ const path = require('path'); const Message = require('../Message'); const MessageCollector = require('../MessageCollector'); const Collection = require('../../util/Collection'); -const util = require('util'); /** * Interface for classes that have text-channel-like features. @@ -38,7 +37,6 @@ class TextBasedChannel { * (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 **(deprecated)** * @property {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if @@ -301,17 +299,6 @@ class TextBasedChannel { return 0; } - /** - * Creates a Message Collector - * @param {CollectorFilter} filter The filter to create the collector with - * @param {MessageCollectorOptions} [options={}] The options to pass to the collector - * @returns {MessageCollector} - * @deprecated - */ - createCollector(filter, options) { - return this.createMessageCollector(filter, options); - } - /** * Creates a Message Collector. * @param {CollectorFilter} filter The filter to create the collector with @@ -398,86 +385,8 @@ class TextBasedChannel { } } -/** @lends TextBasedChannel.prototype */ -const Deprecated = { - /** - * Send a message to this channel. - * @param {StringResolvable} [content] Text for the message - * @param {MessageOptions} [options={}] Options for the message - * @returns {Promise} - * @deprecated - * @example - * // Send a message - * channel.sendMessage('hello!') - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); - */ - sendMessage(content, options) { - return this.send(content, options); - }, - - /** - * Send an embed to this channel. - * @param {RichEmbed|Object} embed Embed for the message - * @param {string} [content] Text for the message - * @param {MessageOptions} [options] Options for the message - * @returns {Promise} - * @deprecated - */ - sendEmbed(embed, content, options) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = ''; - } else if (!options) { - options = {}; - } - return this.send(content, Object.assign(options, { embed })); - }, - - /** - * Send files to this channel. - * @param {FileOptions[]|string[]} files Files to send with the message - * @param {StringResolvable} [content] Text for the message - * @param {MessageOptions} [options] Options for the message - * @returns {Promise} - * @deprecated - */ - sendFiles(files, content, options = {}) { - return this.send(content, Object.assign(options, { files })); - }, - - /** - * Send a file to this channel. - * @param {BufferResolvable} attachment File to send - * @param {string} [name='file.jpg'] Name and extension of the file - * @param {StringResolvable} [content] Text for the message - * @param {MessageOptions} [options] Options for the message - * @returns {Promise} - * @deprecated - */ - sendFile(attachment, name, content, options = {}) { - return this.send({ files: [{ attachment, name }], content, options }); - }, - - /** - * Send a code block to this channel. - * @param {string} lang Language for the code block - * @param {StringResolvable} content Content of the code block - * @param {MessageOptions} [options] Options for the message - * @returns {Promise} - * @deprecated - */ - sendCode(lang, content, options = {}) { - return this.send(content, Object.assign(options, { code: lang })); - }, -}; - -for (const key of Object.keys(Deprecated)) { - TextBasedChannel.prototype[key] = util.deprecate(Deprecated[key], `TextChannel#${key}: use TextChannel#send instead`); -} - exports.applyToClass = (structure, full = false, ignore = []) => { - const props = ['send', 'sendMessage', 'sendEmbed', 'sendFile', 'sendFiles', 'sendCode']; + const props = ['send']; if (full) { props.push( '_cacheMessage', @@ -491,7 +400,6 @@ exports.applyToClass = (structure, full = false, ignore = []) => { 'typing', 'typingCount', 'fetchPinnedMessages', - 'createCollector', 'createMessageCollector', 'awaitMessages' ); diff --git a/src/util/Permissions.js b/src/util/Permissions.js index d5af0c7d1..df438998b 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -1,5 +1,4 @@ const Constants = require('../util/Constants'); -const util = require('util'); /** * Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of @@ -8,19 +7,9 @@ const util = require('util'); */ class Permissions { /** - * @param {GuildMember} [member] Member the permissions are for **(deprecated)** * @param {number|PermissionResolvable[]} permissions Permissions or bitfield to read from */ - constructor(member, permissions) { - permissions = typeof member === 'object' && !(member instanceof Array) ? permissions : member; - - /** - * Member the permissions are for - * @type {GuildMember} - * @deprecated - */ - this._member = typeof member === 'object' ? member : null; - + constructor(permissions) { /** * Bitfield of the packed permissions * @type {number} @@ -28,29 +17,6 @@ class Permissions { this.bitfield = typeof permissions === 'number' ? permissions : this.constructor.resolve(permissions); } - get member() { - return this._member; - } - - set member(value) { - this._member = value; - } - - /** - * Bitfield of the packed permissions - * @type {number} - * @see {@link Permissions#bitfield} - * @deprecated - * @readonly - */ - get raw() { - return this.bitfield; - } - - set raw(raw) { - this.bitfield = raw; - } - /** * Checks whether the bitfield has a permission, or multiple permissions. * @param {PermissionResolvable|PermissionResolvable[]} permission Permission(s) to check for @@ -114,42 +80,6 @@ class Permissions { return serialized; } - /** - * Checks whether the user has a certain permission, e.g. `READ_MESSAGES`. - * @param {PermissionResolvable} permission The permission to check for - * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permission - * @returns {boolean} - * @see {@link Permissions#has} - * @deprecated - */ - hasPermission(permission, explicit = false) { - return this.has(permission, !explicit); - } - - /** - * Checks whether the user has all specified permissions. - * @param {PermissionResolvable[]} permissions The permissions to check for - * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions - * @returns {boolean} - * @see {@link Permissions#has} - * @deprecated - */ - hasPermissions(permissions, explicit = false) { - return this.has(permissions, !explicit); - } - - /** - * Checks whether the user has all specified permissions, and lists any missing permissions. - * @param {PermissionResolvable[]} permissions The permissions to check for - * @param {boolean} [explicit=false] Whether to require the user to explicitly have the exact permissions - * @returns {PermissionResolvable[]} - * @see {@link Permissions#missing} - * @deprecated - */ - missingPermissions(permissions, explicit = false) { - return this.missing(permissions, !explicit); - } - /** * Data that can be resolved to give a permission number. This can be: * - A string (see {@link Permissions.flags}) @@ -189,7 +119,6 @@ class Permissions { * - `READ_MESSAGE_HISTORY` (view messages that were posted prior to opening Discord) * - `MENTION_EVERYONE` * - `USE_EXTERNAL_EMOJIS` (use emojis from different guilds) - * - `EXTERNAL_EMOJIS` **(deprecated)** * - `CONNECT` (connect to a voice channel) * - `SPEAK` (speak in a voice channel) * - `MUTE_MEMBERS` (mute members across all voice channels) @@ -199,7 +128,6 @@ class Permissions { * - `CHANGE_NICKNAME` * - `MANAGE_NICKNAMES` (change other members' nicknames) * - `MANAGE_ROLES` - * - `MANAGE_ROLES_OR_PERMISSIONS` **(deprecated)** * - `MANAGE_WEBHOOKS` * - `MANAGE_EMOJIS` * @type {Object} @@ -223,7 +151,6 @@ Permissions.FLAGS = { ATTACH_FILES: 1 << 15, READ_MESSAGE_HISTORY: 1 << 16, MENTION_EVERYONE: 1 << 17, - EXTERNAL_EMOJIS: 1 << 18, USE_EXTERNAL_EMOJIS: 1 << 18, CONNECT: 1 << 20, @@ -236,7 +163,6 @@ Permissions.FLAGS = { CHANGE_NICKNAME: 1 << 26, MANAGE_NICKNAMES: 1 << 27, MANAGE_ROLES: 1 << 28, - MANAGE_ROLES_OR_PERMISSIONS: 1 << 28, MANAGE_WEBHOOKS: 1 << 29, MANAGE_EMOJIS: 1 << 30, }; @@ -253,23 +179,4 @@ Permissions.ALL = Object.keys(Permissions.FLAGS).reduce((all, p) => all | Permis */ Permissions.DEFAULT = 104324097; -/** - * @class EvaluatedPermissions - * @classdesc The final evaluated permissions for a member in a channel - * @see {@link Permissions} - * @deprecated - */ - -Permissions.prototype.hasPermission = util.deprecate(Permissions.prototype.hasPermission, - 'EvaluatedPermissions#hasPermission is deprecated, use Permissions#has instead'); -Permissions.prototype.hasPermissions = util.deprecate(Permissions.prototype.hasPermissions, - 'EvaluatedPermissions#hasPermissions is deprecated, use Permissions#has instead'); -Permissions.prototype.missingPermissions = util.deprecate(Permissions.prototype.missingPermissions, - 'EvaluatedPermissions#missingPermissions is deprecated, use Permissions#missing instead'); -Object.defineProperty(Permissions.prototype, 'member', { - get: util - .deprecate(Object.getOwnPropertyDescriptor(Permissions.prototype, 'member').get, - 'EvaluatedPermissions#member is deprecated'), -}); - module.exports = Permissions; From 25096047f147da6ec91bf8e7b38e51dfa84be344 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sun, 14 May 2017 15:40:06 -0500 Subject: [PATCH 0034/1359] Fix Permissions now that member is deprecated (#1491) --- src/structures/GuildMember.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 49de7a1b2..a311f0302 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -244,13 +244,13 @@ class GuildMember { * @readonly */ get permissions() { - if (this.user.id === this.guild.ownerID) return new Permissions(this, Permissions.ALL); + if (this.user.id === this.guild.ownerID) return new Permissions(Permissions.ALL); let permissions = 0; const roles = this.roles; for (const role of roles.values()) permissions |= role.permissions; - return new Permissions(this, permissions); + return new Permissions(permissions); } /** From 37a3a6baf97a08e2100211219de6515d048b554e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 14 May 2017 23:44:39 +0200 Subject: [PATCH 0035/1359] removing more deprecation leftovers (#1492) --- src/structures/GuildChannel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 16c8ddd87..9fa7ad80c 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -65,7 +65,7 @@ class GuildChannel extends Channel { permissionsFor(member) { member = this.client.resolver.resolveGuildMember(this.guild, member); if (!member) return null; - if (member.id === this.guild.ownerID) return new Permissions(member, Permissions.ALL); + if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL); let permissions = 0; @@ -94,7 +94,7 @@ class GuildChannel extends Channel { const admin = Boolean(permissions & Permissions.FLAGS.ADMINISTRATOR); if (admin) permissions = Permissions.ALL; - return new Permissions(member, permissions); + return new Permissions(permissions); } overwritesFor(member, verified = false, roles = null) { From a579967eb9d46a8043e86b933736441909924d01 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 15 May 2017 00:48:35 +0200 Subject: [PATCH 0036/1359] Fix MessageCollectors --- src/structures/MessageCollector.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 35a741133..a412f5460 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -33,8 +33,6 @@ class MessageCollector extends Collector { this.received = 0; this.client.on('message', this.listener); - - this.on('collect', this._reEmitter); } /** From 88444cdd2692fe996b4a1c4bc373e66af83cda7b Mon Sep 17 00:00:00 2001 From: Daniel Odendahl Date: Sun, 14 May 2017 19:04:57 -0400 Subject: [PATCH 0037/1359] Fix awaitMessages (#1493) --- src/structures/interfaces/TextBasedChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 226f67b65..eb8296681 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -339,7 +339,7 @@ class TextBasedChannel { */ awaitMessages(filter, options = {}) { return new Promise((resolve, reject) => { - const collector = this.createCollector(filter, options); + const collector = this.createMessageCollector(filter, options); collector.once('end', (collection, reason) => { if (options.errors && options.errors.includes(reason)) { reject(collection); From 20a267c12f3ff959963d44aaf94cf5b249bc9fa2 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 15 May 2017 01:33:10 +0200 Subject: [PATCH 0038/1359] Fix MessageCollector#cleanup --- src/structures/MessageCollector.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index a412f5460..e05929c09 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -67,7 +67,6 @@ class MessageCollector extends Collector { * @private */ cleanup() { - this.removeListener('collect', this._reEmitter); this.client.removeListener('message', this.listener); } } From 8beefe66d5ef9d9a856c432e5937051cef5760fe Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 16 May 2017 04:47:53 +0200 Subject: [PATCH 0039/1359] Fix MessageCollector#postCheck --- src/structures/MessageCollector.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index e05929c09..5ffa2bdd3 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -3,7 +3,7 @@ const Collector = require('./interfaces/Collector'); /** * @typedef {CollectorOptions} MessageCollectorOptions * @property {number} max The maximum amount of messages to process - * @property {number} maxMatches The maximum amount of messages to collect + * @property {number} maxProcessed The maximum amount of messages to collect */ /** @@ -56,9 +56,8 @@ class MessageCollector extends Collector { * @private */ postCheck() { - // Consider changing the end reasons for v12 - if (this.options.maxMatches && this.collected.size >= this.options.max) return 'matchesLimit'; - if (this.options.max && this.received >= this.options.maxProcessed) return 'limit'; + if (this.collected.size >= this.options.max) return 'limit'; + else if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; return null; } From 4955b1ce5fef4325121e9f0920e3e0e85b1fa556 Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 16 May 2017 04:58:13 +0200 Subject: [PATCH 0040/1359] Add max option back for safety --- src/structures/MessageCollector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 5ffa2bdd3..5bcf9ede0 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -56,8 +56,8 @@ class MessageCollector extends Collector { * @private */ postCheck() { - if (this.collected.size >= this.options.max) return 'limit'; - else if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; + if (this.options.max && this.collected.size >= this.options.max) return 'limit'; + if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; return null; } From 3fa87de594ca7603f801d8495416ba4c8d156c13 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 16 May 2017 08:25:42 -0500 Subject: [PATCH 0041/1359] Update Invite.js (#1496) --- src/structures/Invite.js | 45 ++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 995f9c57e..cd9324b44 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -2,27 +2,6 @@ const PartialGuild = require('./PartialGuild'); const PartialGuildChannel = require('./PartialGuildChannel'); const Constants = require('../util/Constants'); -/* -{ max_age: 86400, - code: 'CG9A5', - guild: - { splash: null, - id: '123123123', - icon: '123123123', - name: 'name' }, - created_at: '2016-08-28T19:07:04.763368+00:00', - temporary: false, - uses: 0, - max_uses: 0, - inviter: - { username: '123', - discriminator: '4204', - bot: true, - id: '123123123', - avatar: '123123123' }, - channel: { type: 0, id: '123123', name: 'heavy-testing' } } -*/ - /** * Represents an invitation to a guild channel. * The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing. @@ -60,6 +39,24 @@ class Invite { */ this.presenceCount = data.approximate_presence_count; + /** + * The approximate total number of members of the guild this invite is for + * @type {number} + */ + this.memberCount = data.approximate_member_count; + + /** + * The number of text channels the guild this invite goes to has + * @type {number} + */ + this.textChannelCount = data.guild.text_channel_count; + + /** + * The number of voice channels the guild this invite goes to has + * @type {number} + */ + this.voiceChannelCount = data.guild.voice_channel_count; + /** * Whether or not this invite is temporary * @type {boolean} @@ -72,12 +69,6 @@ class Invite { */ this.maxAge = data.max_age; - /** - * The approximate total number of members of the guild this invite is for - * @type {number} - */ - this.memberCount = data.approximate_member_count; - /** * How many times this invite has been used * @type {number} From 4aa734b9efc8dd0df8d1398183b866a8a2ef17b6 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 16 May 2017 09:11:11 -0500 Subject: [PATCH 0042/1359] guild setPosition missing docs (#1498) * missing docs * update return docs --- src/structures/Guild.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index ec0adb616..df2ee6297 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -648,6 +648,8 @@ class Guild { } /** + * Sets the position of the guild in the guild listing. + * This is only available when using a user account. * @param {number} position Absolute or relative position * @param {boolean} [relative=false] Whether to position relatively or absolutely * @returns {Promise} @@ -662,7 +664,7 @@ class Guild { /** * Marks all messages in this guild as read. * This is only available when using a user account. - * @returns {Promise} This guild + * @returns {Promise} */ acknowledge() { return this.client.rest.methods.ackGuild(this); From 284bec80d4e86d955146c40e15183fb016620e0d Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 20 May 2017 00:03:51 +0200 Subject: [PATCH 0043/1359] Fix MessageCollectorOptions docs --- src/structures/MessageCollector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 5bcf9ede0..8895ea2c1 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -2,8 +2,8 @@ const Collector = require('./interfaces/Collector'); /** * @typedef {CollectorOptions} MessageCollectorOptions - * @property {number} max The maximum amount of messages to process - * @property {number} maxProcessed The maximum amount of messages to collect + * @property {number} max The maximum amount of messages to collect + * @property {number} maxProcessed The maximum amount of messages to process */ /** From 79253544ea033de874f91fac3cbee28aa11935b8 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 20 May 2017 21:04:31 +0200 Subject: [PATCH 0044/1359] Relink permission#FLAGS on docs --- src/util/Permissions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Permissions.js b/src/util/Permissions.js index df438998b..9b72440a2 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -82,7 +82,7 @@ class Permissions { /** * Data that can be resolved to give a permission number. This can be: - * - A string (see {@link Permissions.flags}) + * - A string (see {@link Permissions.FLAGS}) * - A permission number * @typedef {string|number} PermissionResolvable */ From 7934788c107afdfa9d02bb6047644c7304e0b1be Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 20 May 2017 22:19:53 -0500 Subject: [PATCH 0045/1359] you can't mutate a socket event in some browsers (webpack fix) (#1512) --- src/client/websocket/WebSocketConnection.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index b102e05f5..93560a45b 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -284,12 +284,13 @@ class WebSocketConnection extends EventEmitter { * @returns {boolean} */ onMessage(event) { + let data; try { - event.data = this.unpack(event.data); + data = this.unpack(event.data); } catch (err) { this.emit('debug', err); } - return this.onPacket(event.data); + return this.onPacket(data); } /** From 02f03c439f837823b7178043788be902ff009885 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sun, 21 May 2017 06:41:25 +0200 Subject: [PATCH 0046/1359] Update typings --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index b500eb233..54b72a2b7 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit b500eb233182a3c7a5655ae29423844e82e72ab7 +Subproject commit 54b72a2b75abf4f43b2ddd304afaa5c83af51ad2 From 0baa59b679d8542857926d645a02982204b3b80c Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 21 May 2017 00:04:19 -0500 Subject: [PATCH 0047/1359] Internal API Request Rewrite (#1490) * start rewrite * converted guilds * more changes * convert GuildMember * convert User and remove friend methods which kill people * convert more stuff * even more stuff * make things nicer * speed and fixes and stuff * almost finished * fix * Update Client.js * uwu * Update RESTMethods.js * message editing * fix router * fix issue with references * message delete reason * move message sending * fix dm * message splitting * NO MORE REST METHODS * Update Client.js * Update WebhookClient.js * remove all those endpoints from the constants * Update ClientUser.js * Update ClientUser.js * fixes * Update ClientUser.js * complaiancy * all sort of fixes * merge master (#1) * Fix Permissions now that member is deprecated (#1491) * removing more deprecation leftovers (#1492) * Fix MessageCollectors * Fix awaitMessages (#1493) * Fix MessageCollector#cleanup * Fix MessageCollector#postCheck * Add max option back for safety * Update Invite.js (#1496) * guild setPosition missing docs (#1498) * missing docs * update return docs * indent * switched .invites for the apirouter and invite.js * make multiple options an object * Update ClientUser.js * fix nicks * Update WebhookClient.js --- src/client/Client.js | 36 +- src/client/ClientManager.js | 4 +- src/client/WebhookClient.js | 7 + src/client/rest/APIRequest.js | 28 +- src/client/rest/APIRouter.js | 39 + src/client/rest/DiscordAPIError.js | 4 +- src/client/rest/RESTManager.js | 9 +- src/client/rest/RESTMethods.js | 903 ------------------ src/structures/Channel.js | 4 +- src/structures/ClientUser.js | 103 +- src/structures/ClientUserSettings.js | 2 +- src/structures/Emoji.js | 10 +- src/structures/GroupDMChannel.js | 17 +- src/structures/Guild.js | 251 +++-- src/structures/GuildAuditLogs.js | 10 +- src/structures/GuildChannel.js | 48 +- src/structures/GuildMember.js | 39 +- src/structures/Invite.js | 7 +- src/structures/Message.js | 58 +- src/structures/MessageReaction.js | 38 +- src/structures/OAuth2Application.js | 3 +- src/structures/PermissionOverwrites.js | 7 +- src/structures/Role.js | 25 +- src/structures/TextChannel.js | 24 +- src/structures/User.js | 55 +- src/structures/Webhook.js | 73 +- src/structures/interfaces/TextBasedChannel.js | 56 +- src/structures/shared/Search.js | 63 ++ src/structures/shared/SendMessage.js | 60 ++ src/structures/shared/index.js | 4 + src/util/Constants.js | 117 +-- src/util/Util.js | 2 +- test/tester1000.js | 46 + 33 files changed, 849 insertions(+), 1303 deletions(-) create mode 100644 src/client/rest/APIRouter.js delete mode 100644 src/client/rest/RESTMethods.js create mode 100644 src/structures/shared/Search.js create mode 100644 src/structures/shared/SendMessage.js create mode 100644 src/structures/shared/index.js create mode 100644 test/tester1000.js diff --git a/src/client/Client.js b/src/client/Client.js index 861becb2c..c896d00ee 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -12,6 +12,11 @@ const WebSocketManager = require('./websocket/WebSocketManager'); const ActionsManager = require('./actions/ActionsManager'); const Collection = require('../util/Collection'); const Presence = require('../structures/Presence').Presence; +const VoiceRegion = require('../structures/VoiceRegion'); +const Webhook = require('../structures/Webhook'); +const User = require('../structures/User'); +const Invite = require('../structures/Invite'); +const OAuth2Application = require('../structures/OAuth2Application'); const ShardClientUtil = require('../sharding/ShardClientUtil'); const VoiceBroadcast = require('./voice/VoiceBroadcast'); @@ -44,6 +49,13 @@ class Client extends EventEmitter { */ this.rest = new RESTManager(this); + /** + * API shortcut + * @type {Object} + * @private + */ + this.api = this.rest.api; + /** * The data manager of the client * @type {ClientDataManager} @@ -274,7 +286,11 @@ class Client extends EventEmitter { * client.login('my token'); */ login(token) { - return this.rest.methods.login(token); + return new Promise((resolve, reject) => { + if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); + token = token.replace(/^Bot\s*/i, ''); + this.manager.connectToWebSocket(token, resolve, reject); + }); } /** @@ -312,7 +328,9 @@ class Client extends EventEmitter { */ fetchUser(id, cache = true) { if (this.users.has(id)) return Promise.resolve(this.users.get(id)); - return this.rest.methods.getUser(id, cache); + return this.api.users(id).get().then(data => + cache ? this.dataManager.newUser(data) : new User(this, data) + ); } /** @@ -322,7 +340,8 @@ class Client extends EventEmitter { */ fetchInvite(invite) { const code = this.resolver.resolveInviteCode(invite); - return this.rest.methods.getInvite(code); + return this.api.invites(code).get({ query: { with_counts: true } }) + .then(data => new Invite(this, data)); } /** @@ -332,7 +351,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchWebhook(id, token) { - return this.rest.methods.getWebhook(id, token); + return this.api.webhooks(id, token).get().then(data => new Webhook(this.client, data)); } /** @@ -340,7 +359,11 @@ class Client extends EventEmitter { * @returns {Collection} */ fetchVoiceRegions() { - return this.rest.methods.fetchVoiceRegions(); + return this.rest.api.voice.regions.get().then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); } /** @@ -385,7 +408,8 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchApplication(id = '@me') { - return this.rest.methods.getApplication(id); + return this.rest.api.oauth2.applications(id).get() + .then(app => new OAuth2Application(this.client, app)); } /** diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 538411079..4239e2e4a 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -38,7 +38,7 @@ class ClientManager { this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`); this.client.token = token; const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300); - this.client.rest.methods.getGateway().then(res => { + this.client.api.gateway.get().then(res => { const protocolVersion = Constants.DefaultOptions.ws.version; const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`; this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`); @@ -63,7 +63,7 @@ class ClientManager { this.client.token = null; return Promise.resolve(); } else { - return this.client.rest.methods.logout().then(() => { + return this.client.api.logout.post().then(() => { this.client.token = null; }); } diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index f89c8f973..36c945e01 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -34,6 +34,13 @@ class WebhookClient extends Webhook { */ this.rest = new RESTManager(this); + /** + * API shortcut + * @type {Object} + * @private + */ + this.api = this.rest.api; + /** * The data resolver of the client * @type {ClientDataResolver} diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 518017ae3..7271e64b1 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -1,16 +1,15 @@ +const querystring = require('querystring'); const snekfetch = require('snekfetch'); const Constants = require('../../util/Constants'); class APIRequest { - constructor(rest, method, path, auth, data, files) { + constructor(rest, method, path, options) { this.rest = rest; this.client = rest.client; this.method = method; this.path = path.toString(); - this.auth = auth; - this.data = data; - this.files = files; this.route = this.getRoute(this.path); + this.options = options; } getRoute(url) { @@ -34,14 +33,23 @@ class APIRequest { gen() { const API = `${this.client.options.http.host}/api/v${this.client.options.http.version}`; + + if (this.options.query) { + const queryString = (querystring.stringify(this.options.query).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + this.path += `?${queryString}`; + } + const request = snekfetch[this.method](`${API}${this.path}`); - if (this.auth) request.set('Authorization', this.getAuth()); + + if (this.options.auth !== false) request.set('Authorization', this.getAuth()); + if (this.options.reason) request.set('X-Audit-Log-Reason', this.options.reason); if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent); - if (this.files) { - for (const file of this.files) if (file && file.file) request.attach(file.name, file.file, file.name); - if (typeof this.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.data)); - } else if (this.data) { - request.send(this.data); + + if (this.options.files) { + for (const file of this.options.files) if (file && file.file) request.attach(file.name, file.file, file.name); + if (typeof this.options.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.options.data)); + } else if (typeof this.options.data !== 'undefined') { + request.send(this.options.data); } return request; } diff --git a/src/client/rest/APIRouter.js b/src/client/rest/APIRouter.js new file mode 100644 index 000000000..20133822f --- /dev/null +++ b/src/client/rest/APIRouter.js @@ -0,0 +1,39 @@ +const util = require('util'); + +const methods = ['get', 'post', 'delete', 'patch', 'put']; +// Paramable exists so we don't return a function unless we actually need one #savingmemory +const paramable = [ + 'channels', 'users', 'guilds', 'members', + 'bans', 'emojis', 'pins', 'permissions', + 'reactions', 'webhooks', 'messages', + 'notes', 'roles', 'applications', + 'invites', +]; +const reflectors = ['toString', 'valueOf', 'inspect', Symbol.toPrimitive, util.inspect.custom]; + +module.exports = restManager => { + const handler = { + get(list, name) { + if (reflectors.includes(name)) return () => list.join('/'); + if (paramable.includes(name)) { + function toReturn(...args) { // eslint-disable-line no-inner-declarations + list = list.concat(name); + for (const arg of args) { + if (arg !== null && typeof arg !== 'undefined') list = list.concat(arg); + } + return new Proxy(list, handler); + } + const directJoin = () => `${list.join('/')}/${name}`; + for (const r of reflectors) toReturn[r] = directJoin; + for (const method of methods) { + toReturn[method] = options => restManager.request(method, `${list.join('/')}/${name}`, options); + } + return toReturn; + } + if (methods.includes(name)) return options => restManager.request(name, list.join('/'), options); + return new Proxy(list.concat(name), handler); + }, + }; + + return new Proxy([''], handler); +}; diff --git a/src/client/rest/DiscordAPIError.js b/src/client/rest/DiscordAPIError.js index 7d3ff7ef9..c5bd31b15 100644 --- a/src/client/rest/DiscordAPIError.js +++ b/src/client/rest/DiscordAPIError.js @@ -29,8 +29,8 @@ class DiscordAPIError extends Error { if (obj[k]._errors) { messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`); - } else if (obj[k].code && obj[k].message) { - messages.push(`${obj[k].code}: ${obj[k].message}`); + } else if (obj[k].code || obj[k].message) { + messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}${obj[k].message}`.trim()); } else { messages = messages.concat(this.flattenErrors(obj[k], newKey)); } diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 512b3063e..5a7e9f0ed 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -1,8 +1,8 @@ const UserAgentManager = require('./UserAgentManager'); -const RESTMethods = require('./RESTMethods'); const SequentialRequestHandler = require('./RequestHandlers/Sequential'); const BurstRequestHandler = require('./RequestHandlers/Burst'); const APIRequest = require('./APIRequest'); +const mountApi = require('./APIRouter'); const Constants = require('../../util/Constants'); class RESTManager { @@ -10,9 +10,10 @@ class RESTManager { this.client = client; this.handlers = {}; this.userAgentManager = new UserAgentManager(this); - this.methods = new RESTMethods(this); this.rateLimitedEndpoints = {}; this.globallyRateLimited = false; + + this.api = mountApi(this); } destroy() { @@ -42,8 +43,8 @@ class RESTManager { } } - makeRequest(method, url, auth, data, file) { - const apiRequest = new APIRequest(this, method, url, auth, data, file); + request(method, url, options = {}) { + const apiRequest = new APIRequest(this, method, url, options); if (!this.handlers[apiRequest.route]) { const RequestHandlerType = this.getRequestHandler(); this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route); diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js deleted file mode 100644 index 30d5f4ff9..000000000 --- a/src/client/rest/RESTMethods.js +++ /dev/null @@ -1,903 +0,0 @@ -const querystring = require('querystring'); -const long = require('long'); -const Permissions = require('../../util/Permissions'); -const Constants = require('../../util/Constants'); -const Endpoints = Constants.Endpoints; -const Collection = require('../../util/Collection'); -const Snowflake = require('../../util/Snowflake'); -const Util = require('../../util/Util'); - -const User = require('../../structures/User'); -const GuildMember = require('../../structures/GuildMember'); -const Message = require('../../structures/Message'); -const Role = require('../../structures/Role'); -const Invite = require('../../structures/Invite'); -const Webhook = require('../../structures/Webhook'); -const UserProfile = require('../../structures/UserProfile'); -const OAuth2Application = require('../../structures/OAuth2Application'); -const Channel = require('../../structures/Channel'); -const GroupDMChannel = require('../../structures/GroupDMChannel'); -const Guild = require('../../structures/Guild'); -const VoiceRegion = require('../../structures/VoiceRegion'); -const GuildAuditLogs = require('../../structures/GuildAuditLogs'); - -class RESTMethods { - constructor(restManager) { - this.rest = restManager; - this.client = restManager.client; - this._ackToken = null; - } - - login(token = this.client.token) { - return new Promise((resolve, reject) => { - if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); - token = token.replace(/^Bot\s*/i, ''); - this.client.manager.connectToWebSocket(token, resolve, reject); - }); - } - - logout() { - return this.rest.makeRequest('post', Endpoints.logout, true, {}); - } - - getGateway(bot = false) { - return this.rest.makeRequest('get', bot ? Endpoints.gateway.bot : Endpoints.gateway, true); - } - - fetchVoiceRegions(guildID) { - let endpoint; - if (guildID) endpoint = Endpoints.Guild(guildID).voiceRegions; - else endpoint = Endpoints.voiceRegions; - return this.rest.makeRequest('get', endpoint, true).then(res => { - const regions = new Collection(); - for (const region of res) regions.set(region.id, new VoiceRegion(region)); - return regions; - }); - } - - sendMessage(channel, content, { tts, nonce, embed, disableEveryone, split, code, reply } = {}, files = null) { - return new Promise((resolve, reject) => { // eslint-disable-line complexity - if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); - - // The nonce has to be a uint64 :< - if (typeof nonce !== 'undefined') { - nonce = parseInt(nonce); - if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.'); - } - - if (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(this.client.resolver.resolveString(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' && this.client.options.disableEveryone)) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); - } - - // 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 = Util.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 => { - if (content instanceof Array) { - const messages = []; - (function sendChunk(list, index) { - const options = index === list.length ? { tts, embed } : { tts }; - chan.send(list[index], options, index === list.length ? files : null).then(message => { - messages.push(message); - if (index >= list.length - 1) return resolve(messages); - return sendChunk(list, ++index); - }); - }(content, 0)); - } else { - this.rest.makeRequest('post', Endpoints.Channel(chan).messages, true, { - content, tts, nonce, embed, - }, files).then(data => resolve(this.client.actions.MessageCreate.handle(data).message), reject); - } - }; - - if (channel instanceof User || channel instanceof GuildMember) this.createDM(channel).then(send, reject); - else send(channel); - }); - } - - 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 = Util.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', Endpoints.Message(message), true, { - content, embed, - }).then(data => this.client.actions.MessageUpdate.handle(data).updated); - } - - deleteMessage(message) { - return this.rest.makeRequest('delete', Endpoints.Message(message), true) - .then(() => - this.client.actions.MessageDelete.handle({ - id: message.id, - channel_id: message.channel.id, - }).message - ); - } - - ackMessage(message) { - return this.rest.makeRequest('post', Endpoints.Message(message).ack, true, { token: this._ackToken }).then(res => { - if (res.token) this._ackToken = res.token; - return message; - }); - } - - ackTextChannel(channel) { - return this.rest.makeRequest('post', Endpoints.Channel(channel).Message(channel.lastMessageID).ack, true, { - token: this._ackToken, - }).then(res => { - if (res.token) this._ackToken = res.token; - return channel; - }); - } - - ackGuild(guild) { - return this.rest.makeRequest('post', Endpoints.Guild(guild).ack, true).then(() => guild); - } - - bulkDeleteMessages(channel, messages, filterOld) { - if (filterOld) { - messages = messages.filter(id => - Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000 - ); - } - return this.rest.makeRequest('post', Endpoints.Channel(channel).messages.bulkDelete, true, { - messages, - }).then(() => - this.client.actions.MessageDeleteBulk.handle({ - channel_id: channel.id, - ids: messages, - }).messages - ); - } - - 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(); - } - 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(); - } - 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 + 86400000).shiftLeft(22).toString(); - } - if (options.channel) options.channel = this.client.resolver.resolveChannelID(options.channel); - if (options.author) options.author = this.client.resolver.resolveUserID(options.author); - if (options.mentions) options.mentions = this.client.resolver.resolveUserID(options.options.mentions); - options = { - content: options.content, - max_id: options.maxID, - min_id: options.minID, - has: options.has, - channel_id: options.channel, - author_id: options.author, - author_type: options.authorType, - context_size: options.contextSize, - sort_by: options.sortBy, - sort_order: options.sortOrder, - limit: options.limit, - offset: options.offset, - mentions: options.mentions, - mentions_everyone: options.mentionsEveryone, - link_hostname: options.linkHostname, - embed_provider: options.embedProvider, - embed_type: options.embedType, - attachment_filename: options.attachmentFilename, - attachment_extension: options.attachmentExtension, - }; - - for (const key in options) if (options[key] === undefined) delete options[key]; - const queryString = (querystring.stringify(options).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); - - let endpoint; - if (target instanceof Channel) { - endpoint = Endpoints.Channel(target).search; - } else if (target instanceof Guild) { - endpoint = Endpoints.Guild(target).search; - } else { - throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); - } - return this.rest.makeRequest('get', `${endpoint}?${queryString}`, true).then(body => { - const messages = body.messages.map(x => - x.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client)) - ); - return { - totalResults: body.total_results, - messages, - }; - }); - } - - createChannel(guild, channelName, channelType, overwrites) { - if (overwrites instanceof Collection) overwrites = overwrites.array(); - return this.rest.makeRequest('post', Endpoints.Guild(guild).channels, true, { - name: channelName, - type: channelType, - permission_overwrites: overwrites, - }).then(data => this.client.actions.ChannelCreate.handle(data).channel); - } - - createDM(recipient) { - const dmChannel = this.getExistingDM(recipient); - if (dmChannel) return Promise.resolve(dmChannel); - return this.rest.makeRequest('post', Endpoints.User(this.client.user).channels, true, { - recipient_id: recipient.id, - }).then(data => this.client.actions.ChannelCreate.handle(data).channel); - } - - createGroupDM(options) { - const data = this.client.user.bot ? - { access_tokens: options.accessTokens, nicks: options.nicks } : - { recipients: options.recipients }; - return this.rest.makeRequest('post', Endpoints.User('@me').channels, true, data) - .then(res => new GroupDMChannel(this.client, res)); - } - - addUserToGroupDM(channel, options) { - const data = this.client.user.bot ? - { nick: options.nick, access_token: options.accessToken } : - { recipient: options.id }; - return this.rest.makeRequest('put', Endpoints.Channel(channel).Recipient(options.id), true, data) - .then(() => channel); - } - - getExistingDM(recipient) { - return this.client.channels.find(channel => - channel.recipient && channel.recipient.id === recipient.id - ); - } - - deleteChannel(channel) { - if (channel instanceof User || channel instanceof GuildMember) channel = this.getExistingDM(channel); - if (!channel) return Promise.reject(new Error('No channel to delete.')); - return this.rest.makeRequest('delete', Endpoints.Channel(channel), true).then(data => { - data.id = channel.id; - return this.client.actions.ChannelDelete.handle(data).channel; - }); - } - - updateChannel(channel, _data) { - const data = {}; - data.name = (_data.name || channel.name).trim(); - data.topic = _data.topic || channel.topic; - data.position = _data.position || channel.position; - data.bitrate = _data.bitrate || channel.bitrate; - data.user_limit = _data.userLimit || channel.userLimit; - return this.rest.makeRequest('patch', Endpoints.Channel(channel), true, data).then(newData => - this.client.actions.ChannelUpdate.handle(newData).updated - ); - } - - leaveGuild(guild) { - if (guild.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); - return this.rest.makeRequest('delete', Endpoints.User('@me').Guild(guild.id), true).then(() => - this.client.actions.GuildDelete.handle({ id: guild.id }).guild - ); - } - - createGuild(options) { - options.icon = this.client.resolver.resolveBase64(options.icon) || null; - options.region = options.region || 'us-central'; - return new Promise((resolve, reject) => { - this.rest.makeRequest('post', Endpoints.guilds, true, options).then(data => { - if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id)); - - const handleGuild = guild => { - if (guild.id === data.id) { - this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); - this.client.clearTimeout(timeout); - resolve(guild); - } - }; - this.client.on(Constants.Events.GUILD_CREATE, handleGuild); - - const timeout = this.client.setTimeout(() => { - this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); - reject(new Error('Took too long to receive guild data.')); - }, 10000); - return undefined; - }, reject); - }); - } - - // Untested but probably will work - deleteGuild(guild) { - return this.rest.makeRequest('delete', Endpoints.Guild(guild), true).then(() => - this.client.actions.GuildDelete.handle({ id: guild.id }).guild - ); - } - - getUser(userID, cache) { - return this.rest.makeRequest('get', Endpoints.User(userID), true).then(data => { - if (cache) return this.client.actions.UserGet.handle(data).user; - else return new User(this.client, data); - }); - } - - updateCurrentUser(_data, password) { - const user = this.client.user; - const data = {}; - data.username = _data.username || user.username; - data.avatar = this.client.resolver.resolveBase64(_data.avatar) || user.avatar; - if (!user.bot) { - data.email = _data.email || user.email; - data.password = password; - if (_data.new_password) data.new_password = _data.newPassword; - } - return this.rest.makeRequest('patch', Endpoints.User('@me'), true, data).then(newData => - this.client.actions.UserUpdate.handle(newData).updated - ); - } - - updateGuild(guild, _data) { - const data = {}; - if (_data.name) data.name = _data.name; - if (_data.region) data.region = _data.region; - if (_data.verificationLevel) data.verification_level = Number(_data.verificationLevel); - if (_data.afkChannel) data.afk_channel_id = this.client.resolver.resolveChannel(_data.afkChannel).id; - if (_data.afkTimeout) data.afk_timeout = Number(_data.afkTimeout); - if (_data.icon) data.icon = this.client.resolver.resolveBase64(_data.icon); - if (_data.owner) data.owner_id = this.client.resolver.resolveUser(_data.owner).id; - if (_data.splash) data.splash = this.client.resolver.resolveBase64(_data.splash); - return this.rest.makeRequest('patch', Endpoints.Guild(guild), true, data).then(newData => - this.client.actions.GuildUpdate.handle(newData).updated - ); - } - - kickGuildMember(guild, member, reason) { - const url = `${Endpoints.Guild(guild).Member(member)}?reason=${reason}`; - return this.rest.makeRequest('delete', url, true).then(() => - this.client.actions.GuildMemberRemove.handle({ - guild_id: guild.id, - user: member.user, - }).member - ); - } - - createGuildRole(guild, data) { - if (data.color) data.color = this.client.resolver.resolveColor(data.color); - if (data.permissions) data.permissions = Permissions.resolve(data.permissions); - return this.rest.makeRequest('post', Endpoints.Guild(guild).roles, true, data).then(role => - this.client.actions.GuildRoleCreate.handle({ - guild_id: guild.id, - role, - }).role - ); - } - - deleteGuildRole(role) { - return this.rest.makeRequest('delete', Endpoints.Guild(role.guild).Role(role.id), true).then(() => - this.client.actions.GuildRoleDelete.handle({ - guild_id: role.guild.id, - role_id: role.id, - }).role - ); - } - - setChannelOverwrite(channel, payload) { - return this.rest.makeRequest('put', `${Endpoints.Channel(channel).permissions}/${payload.id}`, true, payload); - } - - deletePermissionOverwrites(overwrite) { - return this.rest.makeRequest( - 'delete', `${Endpoints.Channel(overwrite.channel).permissions}/${overwrite.id}`, true - ).then(() => overwrite); - } - - getChannelMessages(channel, payload = {}) { - const params = []; - if (payload.limit) params.push(`limit=${payload.limit}`); - if (payload.around) params.push(`around=${payload.around}`); - else if (payload.before) params.push(`before=${payload.before}`); - else if (payload.after) params.push(`after=${payload.after}`); - - let endpoint = Endpoints.Channel(channel).messages; - if (params.length > 0) endpoint += `?${params.join('&')}`; - return this.rest.makeRequest('get', endpoint, true); - } - - getChannelMessage(channel, messageID) { - const msg = channel.messages.get(messageID); - if (msg) return Promise.resolve(msg); - return this.rest.makeRequest('get', Endpoints.Channel(channel).Message(messageID), true); - } - - putGuildMember(guild, user, options) { - options.access_token = options.accessToken; - if (options.roles) { - const roles = options.roles; - if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { - options.roles = roles.map(role => role.id); - } - } - return this.rest.makeRequest('put', Endpoints.Guild(guild).Member(user.id), true, options) - .then(data => this.client.actions.GuildMemberGet.handle(guild, data).member); - } - - getGuildMember(guild, user, cache) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).Member(user.id), true).then(data => { - if (cache) return this.client.actions.GuildMemberGet.handle(guild, data).member; - else return new GuildMember(guild, data); - }); - } - - updateGuildMember(member, data) { - if (data.channel) { - data.channel_id = this.client.resolver.resolveChannel(data.channel).id; - data.channel = null; - } - if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); - - let endpoint = Endpoints.Member(member); - // Fix your endpoints, discord ;-; - if (member.id === this.client.user.id) { - const keys = Object.keys(data); - if (keys.length === 1 && keys[0] === 'nick') { - endpoint = Endpoints.Member(member).nickname; - } - } - - return this.rest.makeRequest('patch', endpoint, true, data).then(newData => - member.guild._updateMember(member, newData).mem - ); - } - - addMemberRole(member, role) { - return new Promise((resolve, reject) => { - if (member._roles.includes(role.id)) return resolve(member); - - const listener = (oldMember, newMember) => { - if (!oldMember._roles.includes(role.id) && newMember._roles.includes(role.id)) { - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); - resolve(newMember); - } - }; - - this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); - const timeout = this.client.setTimeout(() => - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3); - - return this.rest.makeRequest('put', Endpoints.Member(member).Role(role.id), true).catch(err => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - reject(err); - }); - }); - } - - removeMemberRole(member, role) { - return new Promise((resolve, reject) => { - if (!member._roles.includes(role.id)) return resolve(member); - - const listener = (oldMember, newMember) => { - if (oldMember._roles.includes(role.id) && !newMember._roles.includes(role.id)) { - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener); - resolve(newMember); - } - }; - - this.client.on(Constants.Events.GUILD_MEMBER_UPDATE, listener); - const timeout = this.client.setTimeout(() => - this.client.removeListener(Constants.Events.GUILD_MEMBER_UPDATE, listener), 10e3); - - return this.rest.makeRequest('delete', Endpoints.Member(member).Role(role.id), true).catch(err => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - reject(err); - }); - }); - } - - sendTyping(channelID) { - return this.rest.makeRequest('post', Endpoints.Channel(channelID).typing, true); - } - - banGuildMember(guild, member, options) { - const id = this.client.resolver.resolveUserID(member); - if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); - - const url = `${Endpoints.Guild(guild).bans}/${id}?${querystring.stringify(options)}`; - return this.rest.makeRequest('put', url, true).then(() => { - if (member instanceof GuildMember) return member; - const user = this.client.resolver.resolveUser(id); - if (user) { - member = this.client.resolver.resolveGuildMember(guild, user); - return member || user; - } - return id; - }); - } - - unbanGuildMember(guild, member) { - return new Promise((resolve, reject) => { - const id = this.client.resolver.resolveUserID(member); - if (!id) throw new Error('Couldn\'t resolve the user ID to unban.'); - - const listener = (eGuild, eUser) => { - if (eGuild.id === guild.id && eUser.id === id) { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - resolve(eUser); - } - }; - this.client.on(Constants.Events.GUILD_BAN_REMOVE, listener); - - const timeout = this.client.setTimeout(() => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - reject(new Error('Took too long to receive the ban remove event.')); - }, 10000); - - this.rest.makeRequest('delete', `${Endpoints.Guild(guild).bans}/${id}`, true).catch(err => { - this.client.removeListener(Constants.Events.GUILD_BAN_REMOVE, listener); - this.client.clearTimeout(timeout); - reject(err); - }); - }); - } - - getGuildBans(guild) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(bans => - bans.reduce((collection, ban) => { - collection.set(ban.user.id, { - reason: ban.reason, - user: this.client.dataManager.newUser(ban.user), - }); - return collection; - }, new Collection()) - ); - } - - updateGuildRole(role, _data) { - const data = {}; - data.name = _data.name || role.name; - data.position = typeof _data.position !== 'undefined' ? _data.position : role.position; - 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; - - if (_data.permissions) data.permissions = Permissions.resolve(_data.permissions); - else data.permissions = role.permissions; - - return this.rest.makeRequest('patch', Endpoints.Guild(role.guild).Role(role.id), true, data).then(_role => - this.client.actions.GuildRoleUpdate.handle({ - role: _role, - guild_id: role.guild.id, - }).updated - ); - } - - pinMessage(message) { - return this.rest.makeRequest('put', Endpoints.Channel(message.channel).Pin(message.id), true) - .then(() => message); - } - - unpinMessage(message) { - return this.rest.makeRequest('delete', Endpoints.Channel(message.channel).Pin(message.id), true) - .then(() => message); - } - - getChannelPinnedMessages(channel) { - return this.rest.makeRequest('get', Endpoints.Channel(channel).pins, true); - } - - createChannelInvite(channel, options) { - const payload = {}; - payload.temporary = options.temporary; - payload.max_age = options.maxAge; - payload.max_uses = options.maxUses; - return this.rest.makeRequest('post', Endpoints.Channel(channel).invites, true, payload) - .then(invite => new Invite(this.client, invite)); - } - - deleteInvite(invite) { - return this.rest.makeRequest('delete', Endpoints.Invite(invite.code), true).then(() => invite); - } - - getInvite(code) { - return this.rest.makeRequest('get', Endpoints.Invite(code), true).then(invite => - new Invite(this.client, invite) - ); - } - - getGuildInvites(guild) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).invites, true).then(inviteItems => { - const invites = new Collection(); - for (const inviteItem of inviteItems) { - const invite = new Invite(this.client, inviteItem); - invites.set(invite.code, invite); - } - return invites; - }); - } - - pruneGuildMembers(guild, days, dry) { - return this.rest.makeRequest(dry ? 'get' : 'post', `${Endpoints.Guild(guild).prune}?days=${days}`, true) - .then(data => data.pruned); - } - - createEmoji(guild, image, name, roles) { - const data = { image, name }; - if (roles) data.roles = roles.map(r => r.id ? r.id : r); - return this.rest.makeRequest('post', Endpoints.Guild(guild).emojis, true, data) - .then(emoji => this.client.actions.GuildEmojiCreate.handle(guild, emoji).emoji); - } - - updateEmoji(emoji, _data) { - const data = {}; - if (_data.name) data.name = _data.name; - if (_data.roles) data.roles = _data.roles.map(r => r.id ? r.id : r); - return this.rest.makeRequest('patch', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true, data) - .then(newEmoji => this.client.actions.GuildEmojiUpdate.handle(emoji, newEmoji).emoji); - } - - deleteEmoji(emoji) { - return this.rest.makeRequest('delete', Endpoints.Guild(emoji.guild).Emoji(emoji.id), true) - .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); - } - - getGuildAuditLogs(guild, options = {}) { - if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; - if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; - if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; - - const queryString = (querystring.stringify({ - before: options.before, - after: options.after, - limit: options.limit, - user_id: this.client.resolver.resolveUserID(options.user), - action_type: options.type, - }).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); - - return this.rest.makeRequest('get', `${Endpoints.Guild(guild).auditLogs}?${queryString}`, true) - .then(data => GuildAuditLogs.build(guild, data)); - } - - getWebhook(id, token) { - return this.rest.makeRequest('get', Endpoints.Webhook(id, token), !token).then(data => - new Webhook(this.client, data) - ); - } - - getGuildWebhooks(guild) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).webhooks, true).then(data => { - const hooks = new Collection(); - for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); - return hooks; - }); - } - - getChannelWebhooks(channel) { - return this.rest.makeRequest('get', Endpoints.Channel(channel).webhooks, true).then(data => { - const hooks = new Collection(); - for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); - return hooks; - }); - } - - createWebhook(channel, name, avatar) { - return this.rest.makeRequest('post', Endpoints.Channel(channel).webhooks, true, { name, avatar }) - .then(data => new Webhook(this.client, data)); - } - - editWebhook(webhook, name, avatar) { - return this.rest.makeRequest('patch', Endpoints.Webhook(webhook.id, webhook.token), false, { - name, - avatar, - }).then(data => { - webhook.name = data.name; - webhook.avatar = data.avatar; - return webhook; - }); - } - - deleteWebhook(webhook) { - return this.rest.makeRequest('delete', Endpoints.Webhook(webhook.id, webhook.token), false); - } - - 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)) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); - } - } - return this.rest.makeRequest('post', `${Endpoints.Webhook(webhook.id, webhook.token)}?wait=true`, false, { - username, - avatar_url: avatarURL, - content, - tts, - embeds, - }, file); - } - - sendSlackWebhookMessage(webhook, body) { - return this.rest.makeRequest( - 'post', `${Endpoints.Webhook(webhook.id, webhook.token)}/slack?wait=true`, false, body - ); - } - - fetchUserProfile(user) { - return this.rest.makeRequest('get', Endpoints.User(user).profile, true).then(data => - new UserProfile(user, data) - ); - } - - fetchMentions(options) { - if (options.guild instanceof Guild) options.guild = options.guild.id; - Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - - return this.rest.makeRequest( - 'get', Endpoints.User('@me').Mentions(options.limit, options.roles, options.everyone, options.guild), true - ).then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); - } - - addFriend(user) { - return this.rest.makeRequest('post', Endpoints.User('@me'), true, { - username: user.username, - discriminator: user.discriminator, - }).then(() => user); - } - - removeFriend(user) { - return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) - .then(() => user); - } - - blockUser(user) { - return this.rest.makeRequest('put', Endpoints.User('@me').Relationship(user.id), true, { type: 2 }) - .then(() => user); - } - - unblockUser(user) { - return this.rest.makeRequest('delete', Endpoints.User('@me').Relationship(user.id), true) - .then(() => user); - } - - updateChannelPositions(guildID, channels) { - const data = new Array(channels.length); - for (let i = 0; i < channels.length; i++) { - data[i] = { - id: this.client.resolver.resolveChannelID(channels[i].channel), - position: channels[i].position, - }; - } - - return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, data).then(() => - this.client.actions.GuildChannelsPositionUpdate.handle({ - guild_id: guildID, - channels, - }).guild - ); - } - - setRolePositions(guildID, roles) { - return this.rest.makeRequest('patch', Endpoints.Guild(guildID).roles, true, roles).then(() => - this.client.actions.GuildRolesPositionUpdate.handle({ - guild_id: guildID, - roles, - }).guild - ); - } - - setChannelPositions(guildID, channels) { - return this.rest.makeRequest('patch', Endpoints.Guild(guildID).channels, true, channels).then(() => - this.client.actions.GuildChannelsPositionUpdate.handle({ - guild_id: guildID, - channels, - }).guild - ); - } - - addMessageReaction(message, emoji) { - return this.rest.makeRequest( - 'put', Endpoints.Message(message).Reaction(emoji).User('@me'), true - ).then(() => - message._addReaction(Util.parseEmoji(emoji), message.client.user) - ); - } - - removeMessageReaction(message, emoji, userID) { - const endpoint = Endpoints.Message(message).Reaction(emoji).User(userID === this.client.user.id ? '@me' : userID); - return this.rest.makeRequest('delete', endpoint, true).then(() => - this.client.actions.MessageReactionRemove.handle({ - user_id: userID, - message_id: message.id, - emoji: Util.parseEmoji(emoji), - channel_id: message.channel.id, - }).reaction - ); - } - - removeMessageReactions(message) { - return this.rest.makeRequest('delete', Endpoints.Message(message).reactions, true) - .then(() => message); - } - - getMessageReactionUsers(message, emoji, limit = 100) { - return this.rest.makeRequest('get', Endpoints.Message(message).Reaction(emoji, limit), true); - } - - getApplication(id) { - return this.rest.makeRequest('get', Endpoints.OAUTH2.Application(id), true).then(app => - new OAuth2Application(this.client, app) - ); - } - - resetApplication(id) { - return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).reset, true) - .then(app => new OAuth2Application(this.client, app)); - } - - setNote(user, note) { - return this.rest.makeRequest('put', Endpoints.User(user).note, true, { note }).then(() => user); - } - - acceptInvite(code) { - if (code.id) code = code.id; - return new Promise((resolve, reject) => - this.rest.makeRequest('post', Endpoints.Invite(code), true).then(res => { - const handler = guild => { - if (guild.id === res.id) { - resolve(guild); - this.client.removeListener(Constants.Events.GUILD_CREATE, handler); - } - }; - this.client.on(Constants.Events.GUILD_CREATE, handler); - this.client.setTimeout(() => { - this.client.removeListener(Constants.Events.GUILD_CREATE, handler); - reject(new Error('Accepting invite timed out')); - }, 120e3); - }) - ); - } - - patchUserSettings(data) { - return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data); - } -} - -module.exports = RESTMethods; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 3d492e451..d8f877c9a 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -53,7 +53,7 @@ class Channel { } /** - * Deletes the channel. + * Deletes this channel. * @returns {Promise} * @example * // Delete the channel @@ -62,7 +62,7 @@ class Channel { * .catch(console.error); // Log error */ delete() { - return this.client.rest.methods.deleteChannel(this); + return this.client.api.channels(this.id).delete().then(() => this); } } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index dcd16423c..95d254054 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -2,6 +2,10 @@ const User = require('./User'); const Collection = require('../util/Collection'); const ClientUserSettings = require('./ClientUserSettings'); const Constants = require('../util/Constants'); +const Util = require('../util/Util'); +const Guild = require('./Guild'); +const Message = require('./Message'); +const GroupDMChannel = require('./GroupDMChannel'); /** * Represents the logged in client's Discord user. @@ -75,8 +79,18 @@ class ClientUser extends User { if (data.user_settings) this.settings = new ClientUserSettings(this, data.user_settings); } - edit(data) { - return this.client.rest.methods.updateCurrentUser(data); + edit(data, password) { + const _data = {}; + _data.username = data.username || this.username; + _data.avatar = this.client.resolver.resolveBase64(data.avatar) || this.avatar; + if (!this.bot) { + _data.email = data.email || this.email; + _data.password = password; + if (data.new_password) _data.new_password = data.newPassword; + } + + return this.client.api.users('@me').patch({ data }) + .then(newData => this.client.actions.UserUpdate.handle(newData).updated); } /** @@ -93,7 +107,7 @@ class ClientUser extends User { * .catch(console.error); */ setUsername(username, password) { - return this.client.rest.methods.updateCurrentUser({ username }, password); + return this.edit({ username }, password); } /** @@ -109,7 +123,7 @@ class ClientUser extends User { * .catch(console.error); */ setEmail(email, password) { - return this.client.rest.methods.updateCurrentUser({ email }, password); + return this.edit({ email }, password); } /** @@ -125,7 +139,7 @@ class ClientUser extends User { * .catch(console.error); */ setPassword(newPassword, oldPassword) { - return this.client.rest.methods.updateCurrentUser({ password: newPassword }, oldPassword); + return this.edit({ password: newPassword }, oldPassword); } /** @@ -140,11 +154,10 @@ class ClientUser extends User { */ setAvatar(avatar) { if (typeof avatar === 'string' && avatar.startsWith('data:')) { - return this.client.rest.methods.updateCurrentUser({ avatar }); + return this.edit({ avatar }); } else { - return this.client.resolver.resolveBuffer(avatar).then(data => - this.client.rest.methods.updateCurrentUser({ avatar: data }) - ); + return this.client.resolver.resolveBuffer(avatar) + .then(data => this.edit({ avatar: this.client.resolver.resolveBase64(data) || null })); } } @@ -266,58 +279,42 @@ class ClientUser extends User { * @returns {Promise} */ fetchMentions(options = {}) { - return this.client.rest.methods.fetchMentions(options); - } + if (options.guild instanceof Guild) options.guild = options.guild.id; + Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - /** - * Send a friend request. - * This is only available when using a user account. - * @param {UserResolvable} user The user to send the friend request to - * @returns {Promise} The user the friend request was sent to - */ - addFriend(user) { - user = this.client.resolver.resolveUser(user); - return this.client.rest.methods.addFriend(user); - } - - /** - * Remove a friend. - * This is only available when using a user account. - * @param {UserResolvable} user The user to remove from your friends - * @returns {Promise} The user that was removed - */ - removeFriend(user) { - user = this.client.resolver.resolveUser(user); - return this.client.rest.methods.removeFriend(user); + return this.client.api.users('@me').mentions.get({ query: options }) + .then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); } /** * Creates a guild. * This is only available when using a user account. * @param {string} name The name of the guild - * @param {string} region The region for the server - * @param {BufferResolvable|Base64Resolvable} [icon=null] The icon for the guild + * @param {Object} [options] Options for the creating + * @param {string} [options.region] The region for the server, defaults to the closest one available + * @param {BufferResolvable|Base64Resolvable} [options.icon=null] The icon for the guild * @returns {Promise} The guild that was created */ - createGuild(name, region, icon = null) { - if (!icon) return this.client.rest.methods.createGuild({ name, icon, region }); - if (typeof icon === 'string' && icon.startsWith('data:')) { - return this.client.rest.methods.createGuild({ name, icon, region }); + createGuild(name, { region, icon = null } = {}) { + if (!icon || (typeof icon === 'string' && icon.startsWith('data:'))) { + return this.client.api.guilds.post({ data: { name, region, icon } }) + .then(data => this.client.dataManager.newGuild(data)); } else { - return this.client.resolver.resolveBuffer(icon).then(data => - this.client.rest.methods.createGuild({ name, icon: data, region }) - ); + return this.client.resolver.resolveBuffer(icon) + .then(data => this.createGuild(name, region, this.client.resolver.resolveBase64(data) || null)); } } /** * An object containing either a user or access token, and an optional nickname. * @typedef {Object} GroupDMRecipientOptions - * @property {UserResolvable|Snowflake} [user] User to add to the Group DM + * @property {UserResolvable} [user] User to add to the Group DM * (only available if a user is creating the DM) * @property {string} [accessToken] Access token to use to add a user to the Group DM * (only available if a bot is creating the DM) * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) + * @property {string} [id] If no user resolveable is provided and you want to assign nicknames + * you must provide user ids instead */ /** @@ -326,21 +323,15 @@ class ClientUser extends User { * @returns {Promise} */ createGroupDM(recipients) { - return this.client.rest.methods.createGroupDM({ - recipients: recipients.map(u => this.client.resolver.resolveUserID(u.user)), - accessTokens: recipients.map(u => u.accessToken), - nicks: recipients.map(u => u.nick), - }); - } - - /** - * Accepts an invite to join a guild. - * This is only available when using a user account. - * @param {Invite|string} invite Invite or code to accept - * @returns {Promise} Joined guild - */ - acceptInvite(invite) { - return this.client.rest.methods.acceptInvite(invite); + const data = this.bot ? { + access_tokens: recipients.map(u => u.accessToken), + nicks: recipients.reduce((o, r) => { + if (r.nick) o[r.user ? r.user.id : r.id] = r.nick; + return o; + }, {}), + } : { recipients: recipients.map(u => this.client.resolver.resolveUserID(u)) }; + return this.client.api.users('@me').channels.post({ data }) + .then(res => new GroupDMChannel(this.client, res)); } } diff --git a/src/structures/ClientUserSettings.js b/src/structures/ClientUserSettings.js index 798f348c5..7ba29f71a 100644 --- a/src/structures/ClientUserSettings.js +++ b/src/structures/ClientUserSettings.js @@ -33,7 +33,7 @@ class ClientUserSettings { * @returns {Promise} */ update(name, value) { - return this.user.client.rest.methods.patchUserSettings({ [name]: value }); + return this.user.client.api.users('@me').settings.patch({ data: { [name]: value } }); } /** diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 452dd6654..3010d578b 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -112,6 +112,7 @@ class Emoji { /** * Edits the emoji. * @param {EmojiEditData} data The new data for the emoji + * @param {string} [reason] Reason for editing this emoji * @returns {Promise} * @example * // Edit a emoji @@ -119,8 +120,13 @@ class Emoji { * .then(e => console.log(`Edited emoji ${e}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateEmoji(this, data); + edit(data, reason) { + return this.client.api.guilds(this.guild.id).emojis(this.id) + .patch({ data: { + name: data.name, + roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : [], + }, reason }) + .then(() => this); } /** diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 30a11fc5c..f197b6728 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -126,16 +126,17 @@ class GroupDMChannel extends Channel { /** * Add a user to the DM - * @param {UserResolvable|string} accessTokenOrID Access token or user resolvable + * @param {UserResolvable|string} accessTokenOrUser Access token or user resolvable * @param {string} [nick] Permanent nickname to give the user (only available if a bot is creating the DM) + * @returns {Promise} */ - - addUser(accessTokenOrID, nick) { - return this.client.rest.methods.addUserToGroupDM(this, { - nick, - id: this.client.resolver.resolveUserID(accessTokenOrID), - accessToken: accessTokenOrID, - }); + addUser(accessTokenOrUser, nick) { + const id = this.client.resolver.resolveUserID(accessTokenOrUser); + const data = this.client.user.bot ? + { nick, access_token: accessTokenOrUser } : + { recipient: id }; + return this.client.api.channels(this.id).recipients(id).put({ data }) + .then(() => this); } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index df2ee6297..5f1108292 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -2,12 +2,18 @@ const Long = require('long'); const User = require('./User'); const Role = require('./Role'); const Emoji = require('./Emoji'); +const Invite = require('./Invite'); +const GuildAuditLogs = require('./GuildAuditLogs'); +const Webhook = require('./Webhook'); const Presence = require('./Presence').Presence; const GuildMember = require('./GuildMember'); +const VoiceRegion = require('./VoiceRegion'); const Constants = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const Snowflake = require('../util/Snowflake'); +const Permissions = require('../util/Permissions'); +const Shared = require('./shared'); /** * Represents a guild (or a server) on Discord. @@ -264,7 +270,7 @@ class Guild { size = format; format = 'default'; } - return Constants.Endpoints.Guild(this).Icon(this.client.options.http.cdn, this.icon, format, size); + return Constants.Endpoints.CDN(this.client.options.http.cdn).Icon(this.id, this.icon, format, size); } /** @@ -283,7 +289,7 @@ class Guild { */ get splashURL() { if (!this.splash) return null; - return Constants.Endpoints.Guild(this).Splash(this.client.options.http.cdn, this.splash); + return Constants.Endpoints.CDN(this.client.options.http.cdn).Splash(this.id, this.splash); } /** @@ -370,13 +376,15 @@ class Guild { * @returns {Promise>} */ fetchBans() { - return this.client.rest.methods.getGuildBans(this) - // This entire re-mapping can be removed in the next major release - .then(bans => { - const users = new Collection(); - for (const ban of bans.values()) users.set(ban.user.id, ban.user); - return users; - }); + return this.client.api.guilds(this.id).bans.get().then(bans => + bans.reduce((collection, ban) => { + collection.set(ban.user.id, { + reason: ban.reason, + user: this.client.dataManager.newUser(ban.user), + }); + return collection; + }, new Collection()) + ); } /** @@ -384,7 +392,15 @@ class Guild { * @returns {Promise>} */ fetchInvites() { - return this.client.rest.methods.getGuildInvites(this); + return this.client.api.guilds(this.id).invites.get() + .then(inviteItems => { + const invites = new Collection(); + for (const inviteItem of inviteItems) { + const invite = new Invite(this.client, inviteItem); + invites.set(invite.code, invite); + } + return invites; + }); } /** @@ -392,7 +408,11 @@ class Guild { * @returns {Collection} */ fetchWebhooks() { - return this.client.rest.methods.getGuildWebhooks(this); + return this.client.api.guilds(this.id).webhooks.get().then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); } /** @@ -400,7 +420,11 @@ class Guild { * @returns {Collection} */ fetchVoiceRegions() { - return this.client.rest.methods.fetchVoiceRegions(this.id); + return this.client.api.guilds(this.id).regions.get().then(res => { + const regions = new Collection(); + for (const region of res) regions.set(region.id, new VoiceRegion(region)); + return regions; + }); } /** @@ -413,8 +437,19 @@ class Guild { * @param {string|number} [options.type] Only show entries involving this action type * @returns {Promise} */ - fetchAuditLogs(options) { - return this.client.rest.methods.getGuildAuditLogs(this, options); + fetchAuditLogs(options = {}) { + if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; + if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; + if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; + + return this.client.api.guilds(this.id)['audit-logs'].get({ query: { + before: options.before, + after: options.after, + limit: options.limit, + user_id: this.client.resolver.resolveUserID(options.user), + action_type: options.type, + } }) + .then(data => GuildAuditLogs.build(this, data)); } /** @@ -432,7 +467,15 @@ class Guild { */ addMember(user, options) { if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); - return this.client.rest.methods.putGuildMember(this, user, options); + options.access_token = options.accessToken; + if (options.roles) { + const roles = options.roles; + if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { + options.roles = roles.map(role => role.id); + } + } + return this.client.api.guilds(this.id).members(user.id).put({ data: options }) + .then(data => this.client.actions.GuildMemberGet.handle(this, data).member); } /** @@ -445,7 +488,11 @@ class Guild { user = this.client.resolver.resolveUser(user); if (!user) return Promise.reject(new Error('User is not cached. Use Client.fetchUser first.')); if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); - return this.client.rest.methods.getGuildMember(this, user, cache); + return this.client.api.guilds(this.id).members(user.id).get() + .then(data => { + if (cache) return this.client.actions.GuildMemberGet.handle(this, data).member; + else return new GuildMember(this, data); + }); } /** @@ -502,7 +549,7 @@ class Guild { * }).catch(console.error); */ search(options = {}) { - return this.client.rest.methods.search(this, options); + return Shared.search(this, options); } /** @@ -521,6 +568,7 @@ class Guild { /** * Updates the guild with new information - e.g. a new name. * @param {GuildEditData} data The data to update the guild with + * @param {string} [reason] Reason for editing this guild * @returns {Promise} * @example * // Set the guild name and region @@ -531,8 +579,18 @@ class Guild { * .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateGuild(this, data); + edit(data, reason) { + const _data = {}; + if (data.name) _data.name = data.name; + if (data.region) _data.region = data.region; + if (data.verificationLevel) _data.verification_level = Number(data.verificationLevel); + if (data.afkChannel) _data.afk_channel_id = this.client.resolver.resolveChannel(data.afkChannel).id; + if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); + if (data.icon) _data.icon = this.client.resolver.resolveBase64(data.icon); + if (data.owner) _data.owner_id = this.client.resolver.resolveUser(data.owner).id; + if (data.splash) _data.splash = this.client.resolver.resolveBase64(data.splash); + return this.client.api.guilds(this.id).patch({ data: _data, reason }) + .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } /** @@ -667,7 +725,12 @@ class Guild { * @returns {Promise} */ acknowledge() { - return this.client.rest.methods.ackGuild(this); + return this.client.api.guilds(this.id).ack + .post({ data: { token: this.client.rest._ackToken } }) + .then(res => { + if (res.token) this.client.rest._ackToken = res.token; + return this; + }); } /** @@ -697,19 +760,26 @@ class Guild { * .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`)) * .catch(console.error); */ - ban(user, options = {}) { - if (typeof options === 'number') { - options = { reason: null, 'delete-message-days': options }; - } else if (typeof options === 'string') { - options = { reason: options, 'delete-message-days': 0 }; - } + ban(user, options = { days: 0 }) { if (options.days) options['delete-message-days'] = options.days; - return this.client.rest.methods.banGuildMember(this, user, options); + const id = this.client.resolver.resolveUserID(user); + if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); + return this.client.api.guilds(this.id).bans(id).put({ query: options }) + .then(() => { + if (user instanceof GuildMember) return user; + const _user = this.client.resolver.resolveUser(id); + if (_user) { + const member = this.client.resolver.resolveGuildMember(this, _user); + return member || _user; + } + return id; + }); } /** * Unbans a user from the guild. * @param {UserResolvable} user The user to unban + * @param {string} [reason] Reason for unbanning user * @returns {Promise} * @example * // Unban a user by ID (or with a user/guild member object) @@ -717,29 +787,35 @@ class Guild { * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) * .catch(console.error); */ - unban(user) { - return this.client.rest.methods.unbanGuildMember(this, user); + unban(user, reason) { + const id = this.client.resolver.resolveUserID(user); + if (!id) throw new Error('Couldn\'t resolve the user ID to unban.'); + + return this.client.api.guilds(this.id).bans(id).delete({ reason }) + .then(() => user); } /** * Prunes members from the guild based on how long they have been inactive. - * @param {number} days Number of days of inactivity required to kick - * @param {boolean} [dry=false] If true, will return number of users that will be kicked, without actually doing it + * @param {number} [options.days=7] Number of days of inactivity required to kick + * @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them + * @param {string} [options.reason] Reason for this prune * @returns {Promise} The number of members that were/will be kicked * @example * // See how many members will be pruned - * guild.pruneMembers(12, true) + * guild.pruneMembers({ dry: true }) * .then(pruned => console.log(`This will prune ${pruned} people!`)) * .catch(console.error); * @example * // Actually prune the members - * guild.pruneMembers(12) + * guild.pruneMembers({ days: 1, reason: 'too many people!' }) * .then(pruned => console.log(`I just pruned ${pruned} people!`)) * .catch(console.error); */ - pruneMembers(days, dry = false) { + pruneMembers({ days = 7, dry = false, reason } = {}) { if (typeof days !== 'number') throw new TypeError('Days must be a number.'); - return this.client.rest.methods.pruneGuildMembers(this, days, dry); + return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) + .then(data => data.pruned); } /** @@ -754,7 +830,9 @@ class Guild { * Creates a new channel in the guild. * @param {string} name The name of the new channel * @param {string} type The type of the new channel, either `text` or `voice` - * @param {Array} overwrites Permission overwrites to apply to the new channel + * @param {Object} options Options + * @param {Array} [options.overwrites] Permission overwrites to apply to the new channel + * @param {string} [options.reason] Reason for creating this channel * @returns {Promise} * @example * // Create a new text channel @@ -762,8 +840,14 @@ class Guild { * .then(channel => console.log(`Created new channel ${channel}`)) * .catch(console.error); */ - createChannel(name, type, overwrites) { - return this.client.rest.methods.createChannel(this, name, type, overwrites); + createChannel(name, type, { overwrites, reason } = {}) { + if (overwrites instanceof Collection) overwrites = overwrites.array(); + return this.client.api.guilds(this.id).channels.post({ + data: { + name, type, permission_overwrites: overwrites, + }, + reason, + }).then(data => this.client.actions.ChannelCreate.handle(data).channel); } /** @@ -783,12 +867,30 @@ class Guild { * .catch(console.error); */ setChannelPositions(channelPositions) { - return this.client.rest.methods.updateChannelPositions(this.id, channelPositions); + const data = new Array(channelPositions.length); + for (let i = 0; i < channelPositions.length; i++) { + data[i] = { + id: this.client.resolver.resolveChannelID(channelPositions[i].channel), + position: channelPositions[i].position, + }; + } + + return this.client.api.guilds(this.id).channels.patch({ data: { + guild_id: this.id, + channels: channelPositions, + } }).then(() => + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.id, + channels: channelPositions, + }).guild + ); } /** * Creates a new role in the guild with given information - * @param {RoleData} [data] The data to update the role with + * @param {Object} [options] Options + * @param {RoleData} [options.data] The data to update the role with + * @param {string} [options.reason] Reason for creating this role * @returns {Promise} * @example * // Create a new role @@ -796,16 +898,27 @@ class Guild { * .then(role => console.log(`Created role ${role}`)) * .catch(console.error); * @example - * // Create a new role with data + * // Create a new role with data and a reason * guild.createRole({ - * name: 'Super Cool People', - * color: 'BLUE', + * data: { + * name: 'Super Cool People', + * color: 'BLUE', + * }, + * reason: 'we needed a role for Super Cool People', * }) * .then(role => console.log(`Created role ${role}`)) * .catch(console.error) */ - createRole(data = {}) { - return this.client.rest.methods.createGuildRole(this, data); + createRole({ data = {}, reason } = {}) { + if (data.color) data.color = this.client.resolver.resolveColor(data.color); + if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + + return this.client.api.guilds(this.id).roles.post({ data, reason }).then(role => + this.client.actions.GuildRoleCreate.handle({ + guild_id: this.id, + role, + }).role + ); } /** @@ -826,16 +939,18 @@ class Guild { * .catch(console.error); */ createEmoji(attachment, name, roles) { - return new Promise(resolve => { - if (typeof attachment === 'string' && attachment.startsWith('data:')) { - resolve(this.client.rest.methods.createEmoji(this, attachment, name, roles)); - } else { - this.client.resolver.resolveBuffer(attachment).then(data => { - const dataURI = this.client.resolver.resolveBase64(data); - resolve(this.client.rest.methods.createEmoji(this, dataURI, name, roles)); - }); - } - }); + if (typeof attahment === 'string' && attachment.startsWith('data:')) { + const data = { image: attachment, name }; + if (roles) data.roles = roles.map(r => r.id ? r.id : r); + return this.client.api.guilds(this.id).emojis.post({ data }) + .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); + } else { + return this.client.resolver.resolveBuffer(attachment) + .then(data => { + const dataURI = this.client.resolver.resolveBase64(data); + return this.createEmoji(dataURI, name, roles); + }); + } } /** @@ -845,7 +960,8 @@ class Guild { */ deleteEmoji(emoji) { if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); - return this.client.rest.methods.deleteEmoji(emoji); + return this.client.api.guilds(this.id).emojis(this.id).delete() + .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } /** @@ -858,7 +974,9 @@ class Guild { * .catch(console.error); */ leave() { - return this.client.rest.methods.leaveGuild(this); + if (this.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); + return this.rest.api.users('@me').guilds(this.id).delete() + .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } /** @@ -871,7 +989,8 @@ class Guild { * .catch(console.error); */ delete() { - return this.client.rest.methods.deleteGuild(this); + return this.client.api.guilds(this.id).delete() + .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } /** @@ -1028,7 +1147,13 @@ class Guild { Util.moveElementInArray(updatedRoles, role, position, relative); updatedRoles = updatedRoles.map((r, i) => ({ id: r.id, position: i })); - return this.client.rest.methods.setRolePositions(this.id, updatedRoles); + return this.client.api.guilds(this.id).roles.patch({ data: updatedRoles }) + .then(() => + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.id, + roles: updatedRoles, + }).guild + ); } /** @@ -1052,7 +1177,13 @@ class Guild { Util.moveElementInArray(updatedChannels, channel, position, relative); updatedChannels = updatedChannels.map((r, i) => ({ id: r.id, position: i })); - return this.client.rest.methods.setChannelPositions(this.id, updatedChannels); + return this.client.api.guilds(this.id).channels.patch({ data: updatedChannels }) + .then(() => + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.id, + roles: updatedChannels, + }).guild + ); } /** diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 9202ed231..163c89d01 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -10,6 +10,7 @@ const Targets = { WEBHOOK: 'WEBHOOK', EMOJI: 'EMOJI', MESSAGE: 'MESSAGE', + UNKNOWN: 'UNKNOWN', }; const Actions = { @@ -83,7 +84,7 @@ class GuildAuditLogs { if (target < 60) return Targets.WEBHOOK; if (target < 70) return Targets.EMOJI; if (target < 80) return Targets.MESSAGE; - return null; + return Targets.UNKNOWN; } @@ -219,11 +220,14 @@ class GuildAuditLogsEntry { } } - if ([Targets.USER, Targets.GUILD].includes(targetType)) { + + if (targetType === Targets.UNKNOWN) { /** * The target of this entry - * @type {?Guild|User|Role|Emoji|Invite|Webhook} + * @type {Snowflake|Guild|User|Role|Emoji|Invite|Webhook} */ + this.target = data.target_id; + } else if ([Targets.USER, Targets.GUILD].includes(targetType)) { this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = guild.fetchWebhooks() diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 9fa7ad80c..1d5750a3d 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -1,5 +1,6 @@ const Channel = require('./Channel'); const Role = require('./Role'); +const Invite = require('./Invite'); const PermissionOverwrites = require('./PermissionOverwrites'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); @@ -138,7 +139,8 @@ class GuildChannel extends Channel { * Overwrites the permissions for a user or role in this channel. * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update * @param {PermissionOverwriteOptions} options The configuration for the update - * @returns {Promise} + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise} * @example * // Overwrite permissions for a message author * message.channel.overwritePermissions(message.author, { @@ -147,7 +149,7 @@ class GuildChannel extends Channel { * .then(() => console.log('Done!')) * .catch(console.error); */ - overwritePermissions(userOrRole, options) { + overwritePermissions(userOrRole, options, reason) { const payload = { allow: 0, deny: 0, @@ -186,7 +188,9 @@ class GuildChannel extends Channel { } } - return this.client.rest.methods.setChannelOverwrite(this, payload); + return this.client.api.channels(this.id).permissions(payload.id) + .put({ data: payload, reason }) + .then(() => this); } /** @@ -202,6 +206,7 @@ class GuildChannel extends Channel { /** * Edits the channel. * @param {ChannelData} data The new data for the channel + * @param {string} [reason] Reason for editing this channel * @returns {Promise} * @example * // Edit a channel @@ -209,8 +214,17 @@ class GuildChannel extends Channel { * .then(c => console.log(`Edited channel ${c}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateChannel(this, data); + edit(data, reason) { + return this.client.api.channels(this.id).patch({ + data: { + name: (data.name || this.name).trim(), + topic: data.topic || this.topic, + position: data.position || this.position, + bitrate: data.bitrate || this.bitrate, + user_limit: data.userLimit || this.userLimit, + }, + reason, + }).then(newData => this.client.actions.ChannelUpdate.handle(newData).updated); } /** @@ -253,7 +267,7 @@ class GuildChannel extends Channel { * .catch(console.error); */ setTopic(topic) { - return this.client.rest.methods.updateChannel(this, { topic }); + return this.edit({ topic }); } /** @@ -269,10 +283,14 @@ class GuildChannel extends Channel { * kicked after 24 hours if they have not yet received a role * @param {number} [options.maxAge=86400] How long the invite should last (in seconds, 0 for forever) * @param {number} [options.maxUses=0] Maximum number of uses + * @param {string} [reason] Reason for creating this * @returns {Promise} */ - createInvite(options = {}) { - return this.client.rest.methods.createChannelInvite(this, options); + createInvite({ temporary = false, maxAge = 86400, maxUses = 0 }, reason) { + return this.client.api.channels(this.id).invites.post({ data: { + temporary, max_age: maxAge, max_uses: maxUses, + }, reason }) + .then(invite => new Invite(this.client, invite)); } /** @@ -322,6 +340,20 @@ class GuildChannel extends Channel { this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS); } + /** + * Deletes this channel. + * @param {string} [reason] Reason for deleting this channel + * @returns {Promise} + * @example + * // Delete the channel + * channel.delete('making room for new channels') + * .then() // Success + * .catch(console.error); // Log error + */ + delete(reason) { + return this.client.api.channels(this.id).delete({ reason }).then(() => this); + } + /** * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. * @returns {string} diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index a311f0302..201a2b792 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -332,10 +332,24 @@ class GuildMember { /** * Edit a guild member. * @param {GuildMemberEditData} data The data to edit the member with + * @param {string} [reason] Reason for editing this user * @returns {Promise} */ - edit(data) { - return this.client.rest.methods.updateGuildMember(this, data); + edit(data, reason) { + if (data.channel) { + data.channel_id = this.client.resolver.resolveChannel(data.channel).id; + data.channel = null; + } + if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); + let endpoint = this.client.api.guilds(this.guild.id); + if (this.user.id === this.client.user.id) { + const keys = Object.keys(data); + if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me').nick; + else endpoint = endpoint.members(this.id); + } else { + endpoint = endpoint.members(this.id); + } + return endpoint.patch({ data, reason }).then(newData => this.guild._updateMember(this, newData).mem); } /** @@ -382,7 +396,10 @@ class GuildMember { addRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); - return this.client.rest.methods.addMemberRole(this, role); + if (this._roles.includes(role.id)) return Promise.resolve(this); + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) + .put() + .then(() => this); } /** @@ -394,9 +411,9 @@ class GuildMember { let allRoles; if (roles instanceof Collection) { allRoles = this._roles.slice(); - for (const role of roles.values()) allRoles.push(role.id); + for (const role of roles.values()) allRoles.push(role.id ? role.id : role); } else { - allRoles = this._roles.concat(roles); + allRoles = this._roles.concat(roles.map(r => r.id ? r.id : r)); } return this.edit({ roles: allRoles }); } @@ -409,7 +426,9 @@ class GuildMember { removeRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); - return this.client.rest.methods.removeMemberRole(this, role); + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) + .delete() + .then(() => this); } /** @@ -464,7 +483,13 @@ class GuildMember { * @returns {Promise} */ kick(reason) { - return this.client.rest.methods.kickGuildMember(this.guild, this, reason); + return this.client.api.guilds(this.guild.id).members(this.user.id).delete({ reason }) + .then(() => + this.client.actions.GuildMemberRemove.handle({ + guild_id: this.guild.id, + user: this.user, + }).member + ); } /** diff --git a/src/structures/Invite.js b/src/structures/Invite.js index cd9324b44..db2cbf7dc 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -136,15 +136,16 @@ class Invite { * @readonly */ get url() { - return Constants.Endpoints.inviteLink(this.code); + return Constants.Endpoints.invite(this.code); } /** * Deletes this invite. + * @param {string} [reason] Reason for deleting this invite * @returns {Promise} */ - delete() { - return this.client.rest.methods.deleteInvite(this); + delete(reason) { + return this.client.api.invites(this.code).delete({ reason }).then(() => this); } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index 8ca7b75a1..4881fe127 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -380,7 +380,27 @@ class Message { } else if (!options) { options = {}; } - return this.client.rest.methods.updateMessage(this, content, options); + + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + + const { embed, code, reply } = options; + + // Wrap everything in a code block + if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { + content = Util.escapeMarkdown(this.client.resolver.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.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + content = `${mention}${content ? `, ${content}` : ''}`; + } + + return this.client.api.channels(this.channel.id).messages(this.id) + .patch({ data: { content, embed } }) + .then(data => this.client.actions.MessageUpdate.handle(data).updated); } /** @@ -388,7 +408,8 @@ class Message { * @returns {Promise} */ pin() { - return this.client.rest.methods.pinMessage(this); + return this.client.api.channels(this.channel.id).pins(this.id).put() + .then(() => this); } /** @@ -396,7 +417,8 @@ class Message { * @returns {Promise} */ unpin() { - return this.client.rest.methods.unpinMessage(this); + return this.client.api.channels(this.channel.id).pins(this.id).delete() + .then(() => this); } /** @@ -408,7 +430,9 @@ class Message { emoji = this.client.resolver.resolveEmojiIdentifier(emoji); if (!emoji) throw new TypeError('Emoji must be a string or Emoji/ReactionEmoji'); - return this.client.rest.methods.addMessageReaction(this, emoji); + return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji)['@me'] + .put() + .then(() => this._addReaction(Util.parseEmoji(emoji), this.client.user)); } /** @@ -416,12 +440,15 @@ class Message { * @returns {Promise} */ clearReactions() { - return this.client.rest.methods.removeMessageReactions(this); + return this.client.api.channels(this.channel.id).messages(this.id).reactions.delete() + .then(() => this); } /** * Deletes the message. - * @param {number} [timeout=0] How long to wait to delete the message in milliseconds + * @param {Object} [options] Options + * @param {number} [options.timeout=0] How long to wait to delete the message in milliseconds + * @param {string} [options.reason] Reason for deleting this message, if it does not belong to the client user * @returns {Promise} * @example * // Delete a message @@ -429,13 +456,19 @@ class Message { * .then(msg => console.log(`Deleted message from ${msg.author}`)) * .catch(console.error); */ - delete(timeout = 0) { + delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.client.rest.methods.deleteMessage(this); + return this.client.api.channels(this.channel.id).messages(this.id) + .delete({ reason }) + .then(() => + this.client.actions.MessageDelete.handle({ + id: this.id, + channel_id: this.channel.id, + }).message); } else { return new Promise(resolve => { this.client.setTimeout(() => { - resolve(this.delete()); + resolve(this.delete({ reason })); }, timeout); }); } @@ -468,7 +501,12 @@ class Message { * @returns {Promise} */ acknowledge() { - return this.client.rest.methods.ackMessage(this); + return this.client.api.channels(this.channel.id).messages(this.id).ack + .post({ data: { token: this.client.rest._ackToken } }) + .then(res => { + if (res.token) this.client.rest._ackToken = res.token; + return this; + }); } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 251bafcc0..87178aa20 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -61,12 +61,19 @@ class MessageReaction { * @returns {Promise} */ remove(user = this.message.client.user) { - const message = this.message; const userID = this.message.client.resolver.resolveUserID(user); if (!userID) return Promise.reject(new Error('Couldn\'t resolve the user ID to remove from the reaction.')); - return message.client.rest.methods.removeMessageReaction( - message, this.emoji.identifier, userID - ); + return this.message.client.api.channels(this.message.channel.id).messages(this.message.id) + .reactions(this.emoji.identifier)[userID === this.message.client.user.id ? '@me' : userID] + .delete() + .then(() => + this.message.client.actions.MessageReactionRemove.handle({ + user_id: userID, + message_id: this.message.id, + emoji: this.emoji, + channel_id: this.message.channel.id, + }).reaction + ); } /** @@ -76,17 +83,18 @@ class MessageReaction { */ fetchUsers(limit = 100) { const message = this.message; - return message.client.rest.methods.getMessageReactionUsers( - message, this.emoji.identifier, limit - ).then(users => { - this.users = new Collection(); - for (const rawUser of users) { - const user = this.message.client.dataManager.newUser(rawUser); - this.users.set(user.id, user); - } - this.count = this.users.size; - return this.users; - }); + return message.client.api.channels(message.channel.id).messages(message.id) + .reactions(this.emoji.identifier) + .get({ query: { limit } }) + .then(users => { + this.users = new Collection(); + for (const rawUser of users) { + const user = message.client.dataManager.newUser(rawUser); + this.users.set(user.id, user); + } + this.count = this.users.size; + return this.users; + }); } } diff --git a/src/structures/OAuth2Application.js b/src/structures/OAuth2Application.js index b6f64dd3b..52eb89a12 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/OAuth2Application.js @@ -137,7 +137,8 @@ class OAuth2Application { * @returns {OAuth2Application} */ reset() { - return this.client.rest.methods.resetApplication(this.id); + return this.rest.api.oauth2.applications(this.id).reset.post() + .then(app => new OAuth2Application(this.client, app)); } /** diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 8044be45a..3ffcaf98b 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -33,10 +33,13 @@ class PermissionOverwrites { /** * Delete this Permission Overwrite. + * @param {string} [reason] Reason for deleting this overwrite * @returns {Promise} */ - delete() { - return this.channel.client.rest.methods.deletePermissionOverwrites(this); + delete(reason) { + return this.channel.client.api.channels(this.channel.id).permissions(this.id) + .delete({ reason }) + .then(() => this); } } diff --git a/src/structures/Role.js b/src/structures/Role.js index 22594fd8c..40d997fe7 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -190,6 +190,7 @@ class Role { /** * Edits the role. * @param {RoleData} data The new data for the role + * @param {string} [reason] Reason for editing this role * @returns {Promise} * @example * // Edit a role @@ -197,8 +198,20 @@ class Role { * .then(r => console.log(`Edited role ${r}`)) * .catch(console.error); */ - edit(data) { - return this.client.rest.methods.updateGuildRole(this, data); + edit(data, reason) { + if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + else data.permissions = this.permissions; + return this.client.api.guilds(this.guild.id).roles(this.id).patch({ + data: { + name: data.name || this.name, + position: typeof data.position !== 'undefined' ? data.position : this.position, + color: this.client.resolver.resolveColor(data.color || this.color), + hoist: typeof data.hoist !== 'undefined' ? data.hoist : this.hoist, + mentionable: typeof data.mentionable !== 'undefined' ? data.mentionable : this.mentionable, + }, + reason, + }) + .then(role => this.client.actions.GuildRoleUpdate.handle({ role, guild_id: this.guild.id }).updated); } /** @@ -288,6 +301,7 @@ class Role { /** * Deletes the role. + * @param {string} [reason] Reason for deleting this role * @returns {Promise} * @example * // Delete a role @@ -295,8 +309,11 @@ class Role { * .then(r => console.log(`Deleted role ${r}`)) * .catch(console.error); */ - delete() { - return this.client.rest.methods.deleteGuildRole(this); + delete(reason) { + return this.client.api.guilds(this.guild.id).roles(this.id).delete({ reason }) + .then(() => + this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: this.id }).role + ); } /** diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 818b9217a..0f0d05ba9 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -1,4 +1,5 @@ const GuildChannel = require('./GuildChannel'); +const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); @@ -56,7 +57,11 @@ class TextChannel extends GuildChannel { * @returns {Promise>} */ fetchWebhooks() { - return this.client.rest.methods.getChannelWebhooks(this); + return this.client.api.channels(this.id).webhooks.get().then(data => { + const hooks = new Collection(); + for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + }); } /** @@ -70,15 +75,14 @@ class TextChannel extends GuildChannel { * .catch(console.error) */ createWebhook(name, avatar) { - return new Promise(resolve => { - if (typeof avatar === 'string' && avatar.startsWith('data:')) { - resolve(this.client.rest.methods.createWebhook(this, name, avatar)); - } else { - this.client.resolver.resolveBuffer(avatar).then(data => - resolve(this.client.rest.methods.createWebhook(this, name, data)) - ); - } - }); + if (typeof avatar === 'string' && avatar.startsWith('data:')) { + return this.client.api.channels(this.id).webhooks.post({ data: { + name, avatar, + } }).then(data => new Webhook(this.client, data)); + } else { + return this.client.resolver.resolveBuffer(avatar).then(data => + this.createWebhook(name, this.client.resolver.resolveBase64(data) || null)); + } } // These are here only for documentation purposes - they are implemented by TextBasedChannel diff --git a/src/structures/User.js b/src/structures/User.js index b7b6eb96d..2797f7ff9 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -1,6 +1,7 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Constants = require('../util/Constants'); const Presence = require('./Presence').Presence; +const UserProfile = require('./UserProfile'); const Snowflake = require('../util/Snowflake'); /** @@ -115,7 +116,7 @@ class User { size = format; format = 'default'; } - return Constants.Endpoints.User(this).Avatar(this.client.options.http.cdn, this.avatar, format, size); + return Constants.Endpoints.CDN(this.client.options.http.cdn).Avatar(this.id, this.avatar, format, size); } /** @@ -199,7 +200,11 @@ class User { * @returns {Promise} */ createDM() { - return this.client.rest.methods.createDM(this); + if (this.dmChannel) return Promise.resolve(this.dmChannel); + return this.client.api.users(this.client.user.id).channels.post({ data: { + recipient_id: this.id, + } }) + .then(data => this.client.actions.ChannelCreate.handle(data).channel); } /** @@ -207,43 +212,10 @@ class User { * @returns {Promise} */ deleteDM() { - return this.client.rest.methods.deleteChannel(this); - } - - /** - * Sends a friend request to the user. - * This is only available when using a user account. - * @returns {Promise} - */ - addFriend() { - return this.client.rest.methods.addFriend(this); - } - - /** - * Removes the user from your friends. - * This is only available when using a user account. - * @returns {Promise} - */ - removeFriend() { - return this.client.rest.methods.removeFriend(this); - } - - /** - * Blocks the user. - * This is only available when using a user account. - * @returns {Promise} - */ - block() { - return this.client.rest.methods.blockUser(this); - } - - /** - * Unblocks the user. - * This is only available when using a user account. - * @returns {Promise} - */ - unblock() { - return this.client.rest.methods.unblockUser(this); + if (!this.dmChannel) return Promise.reject(new Error('No DM Channel exists!')); + return this.client.api.channels(this.dmChannel.id).delete().then(data => + this.client.actions.ChannelDelete.handle(data).channel + ); } /** @@ -252,7 +224,7 @@ class User { * @returns {Promise} */ fetchProfile() { - return this.client.rest.methods.fetchUserProfile(this); + return this.client.api.users(this.id).profile.get().then(data => new UserProfile(data)); } /** @@ -262,7 +234,8 @@ class User { * @returns {Promise} */ setNote(note) { - return this.client.rest.methods.setNote(this, note); + return this.client.api.users('@me').notes(this.id).put({ data: { note } }) + .then(() => this); } /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index ca20b0943..3a6bc5b6c 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -91,7 +91,7 @@ class Webhook { * Send a message with this webhook. * @param {StringResolvable} [content] The content to send * @param {WebhookMessageOptions} [options={}] The options to provide - * @returns {Promise} + * @returns {Promise} * @example * // Send a message * webhook.send('hello!') @@ -106,6 +106,23 @@ class Webhook { options = {}; } + if (!options.username) options.username = this.name; + + if (options.avatarURL) { + options.avatar_url = options.avatarURL; + options.avatarURL = null; + } + + if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + if (content) { + if (options.disableEveryone || + (typeof options.disableEveryone === 'undefined' && this.client.options.disableEveryone) + ) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + } + options.content = content; + if (options.file) { if (options.files) options.files.push(options.file); else options.files = [options.file]; @@ -132,16 +149,29 @@ class Webhook { file.file = buffer; return file; }) - )).then(files => this.client.rest.methods.sendWebhookMessage(this, content, options, files)); + )).then(files => this.client.api.webhooks(this.id, this.token).post({ + data: options, + query: { wait: true }, + files, + auth: false, + })); } - return this.client.rest.methods.sendWebhookMessage(this, content, options); + return this.client.api.webhooks(this.id, this.token).post({ + data: options, + query: { wait: true }, + auth: false, + }).then(data => { + if (!this.client.channels) return data; + const Message = require('./Message'); + return new Message(this.client.channels.get(data.channel_id, data, this.client)); + }); } /** * Send a raw slack message with this webhook. * @param {Object} body The raw body to send - * @returns {Promise} + * @returns {Promise} * @example * // Send a slack message * webhook.sendSlackMessage({ @@ -156,34 +186,49 @@ class Webhook { * }).catch(console.error); */ sendSlackMessage(body) { - return this.client.rest.methods.sendSlackWebhookMessage(this, body); + return this.client.api.webhooks(this.id, this.token).slack.post({ + query: { wait: true }, + auth: false, + data: body, + }).then(data => { + if (!this.client.channels) return data; + const Message = require('./Message'); + return new Message(this.client.channels.get(data.channel_id, data, this.client)); + }); } /** * Edit the webhook. - * @param {string} name The new name for the webhook - * @param {BufferResolvable} avatar The new avatar for the webhook + * @param {Object} options Options + * @param {string} [options.name] New name for this webhook + * @param {BufferResolvable} [options.avatar] New avatar for this webhook + * @param {string} [reason] Reason for editing this webhook * @returns {Promise} */ - edit(name = this.name, avatar) { - if (avatar) { + edit({ name = this.name, avatar }, reason) { + if (avatar && (typeof avatar === 'string' && !avatar.startsWith('data:'))) { return this.client.resolver.resolveBuffer(avatar).then(file => { const dataURI = this.client.resolver.resolveBase64(file); - return this.client.rest.methods.editWebhook(this, name, dataURI); + return this.edit({ name, avatar: dataURI }, reason); }); } - return this.client.rest.methods.editWebhook(this, name).then(data => { - this.setup(data); + return this.client.api.webhooks(this.id, this.token).patch({ + data: { name, avatar }, + reason, + }).then(data => { + this.name = data.name; + this.avatar = data.avatar; return this; }); } /** * Delete the webhook. + * @param {string} [reason] Reason for deleting this webhook * @returns {Promise} */ - delete() { - return this.client.rest.methods.deleteWebhook(this); + delete(reason) { + return this.client.api.webhooks(this.id, this.token).delete({ reason }); } } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index eb8296681..7c3b64fda 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,7 +1,8 @@ const path = require('path'); -const Message = require('../Message'); const MessageCollector = require('../MessageCollector'); +const Shared = require('../shared'); const Collection = require('../../util/Collection'); +const Snowflake = require('../../util/Snowflake'); /** * Interface for classes that have text-channel-like features. @@ -78,6 +79,8 @@ class TextBasedChannel { options = {}; } + if (!options.content) options.content = content; + if (options.embed && options.embed.file) options.file = options.embed.file; if (options.file) { @@ -106,10 +109,13 @@ class TextBasedChannel { file.file = buffer; return file; }) - )).then(files => this.client.rest.methods.sendMessage(this, content, options, files)); + )).then(files => { + options.files = files; + return Shared.sendMessage(this, options); + }); } - return this.client.rest.methods.sendMessage(this, content, options); + return Shared.sendMessage(this, options); } /** @@ -125,14 +131,17 @@ class TextBasedChannel { * .catch(console.error); */ fetchMessage(messageID) { + const Message = require('../Message'); if (!this.client.user.bot) { - return this.fetchMessages({ limit: 1, around: messageID }).then(messages => { + return this.fetchMessages({ limit: 1, around: messageID }) + .then(messages => { const msg = messages.get(messageID); if (!msg) throw new Error('Message not found.'); return msg; }); } - return this.client.rest.methods.getChannelMessage(this, messageID).then(data => { + return this.client.api.channels(this.id).messages(messageID).get() + .then(data => { const msg = data instanceof Message ? data : new Message(this, data, this.client); this._cacheMessage(msg); return msg; @@ -160,7 +169,9 @@ class TextBasedChannel { * .catch(console.error); */ fetchMessages(options = {}) { - return this.client.rest.methods.getChannelMessages(this, options).then(data => { + const Message = require('../Message'); + return this.client.api.channels(this.id).messages.get({ query: options }) + .then(data => { const messages = new Collection(); for (const message of data) { const msg = new Message(this, message, this.client); @@ -176,7 +187,8 @@ class TextBasedChannel { * @returns {Promise>} */ fetchPinnedMessages() { - return this.client.rest.methods.getChannelPinnedMessages(this).then(data => { + const Message = require('../Message'); + return this.client.api.channels(this.id).pins.get().then(data => { const messages = new Collection(); for (const message of data) { const msg = new Message(this, message, this.client); @@ -231,7 +243,7 @@ class TextBasedChannel { * }).catch(console.error); */ search(options = {}) { - return this.client.rest.methods.search(this, options); + return Shared.search(this, options); } /** @@ -244,13 +256,14 @@ class TextBasedChannel { startTyping(count) { if (typeof count !== 'undefined' && count < 1) throw new RangeError('Count must be at least 1.'); 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(() => { - this.client.rest.methods.sendTyping(this.id); + endpoint.post(); }, 9000), }); - this.client.rest.methods.sendTyping(this.id); + endpoint.post(); } else { const entry = this.client.user._typing.get(this.id); entry.count = count || entry.count + 1; @@ -360,8 +373,20 @@ class TextBasedChannel { bulkDelete(messages, filterOld = false) { if (!isNaN(messages)) return this.fetchMessages({ limit: messages }).then(msgs => this.bulkDelete(msgs, filterOld)); if (messages instanceof Array || messages instanceof Collection) { - const messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id); - return this.client.rest.methods.bulkDeleteMessages(this, messageIDs, filterOld); + let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id); + if (filterOld) { + messageIDs = messageIDs.filter(id => + Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000 + ); + } + return this.rest.api.channels(this.id).messages['bulk-delete'] + .post({ data: { messages: messageIDs } }) + .then(() => + this.client.actions.MessageDeleteBulk.handle({ + channel_id: this.id, + ids: messageIDs, + }).messages + ); } throw new TypeError('The messages must be an Array, Collection, or number.'); } @@ -373,7 +398,12 @@ class TextBasedChannel { */ acknowledge() { if (!this.lastMessageID) return Promise.resolve(this); - return this.client.rest.methods.ackTextChannel(this); + return this.client.api.channels(this.id).messages(this.lastMessageID) + .post({ data: { token: this.client.rest._ackToken } }) + .then(res => { + if (res.token) this.client.rest._ackToken = res.token; + return this; + }); } _cacheMessage(message) { diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js new file mode 100644 index 000000000..fc4b20c3c --- /dev/null +++ b/src/structures/shared/Search.js @@ -0,0 +1,63 @@ +const long = require('long'); + +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(); + } + 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(); + } + 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 + 86400000).shiftLeft(22).toString(); + } + if (options.channel) options.channel = target.client.resolver.resolveChannelID(options.channel); + if (options.author) options.author = target.client.resolver.resolveUserID(options.author); + if (options.mentions) options.mentions = target.client.resolver.resolveUserID(options.options.mentions); + options = { + content: options.content, + max_id: options.maxID, + min_id: options.minID, + has: options.has, + channel_id: options.channel, + author_id: options.author, + author_type: options.authorType, + context_size: options.contextSize, + sort_by: options.sortBy, + sort_order: options.sortOrder, + limit: options.limit, + offset: options.offset, + mentions: options.mentions, + mentions_everyone: options.mentionsEveryone, + link_hostname: options.linkHostname, + embed_provider: options.embedProvider, + embed_type: options.embedType, + attachment_filename: options.attachmentFilename, + attachment_extension: options.attachmentExtension, + }; + + // Lazy load these because some of them use util + const Channel = require('../Channel'); + const Guild = require('../Guild'); + const Message = require('../Message'); + + if (!(target instanceof Channel || target instanceof Guild)) { + throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); + } + + let endpoint = target.client.api[target instanceof Channel ? 'channels' : 'guilds'](target.id).messages().search; + return endpoint.get({ query: options }).then(body => { + const messages = body.messages.map(x => + x.map(m => new Message(target.client.channels.get(m.channel_id), m, target.client)) + ); + return { + totalResults: body.total_results, + messages, + }; + }); +}; diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js new file mode 100644 index 000000000..cfeef8ef9 --- /dev/null +++ b/src/structures/shared/SendMessage.js @@ -0,0 +1,60 @@ +const Util = require('../../util/Util'); + +module.exports = function sendMessage(channel, options) { + const User = require('../User'); + if (channel instanceof User) return channel.createDM().then(dm => dm.send(options)); + const GuildMember = require('../GuildMember'); + let { content, nonce, reply, code, disableEveryone, tts, embed, files, split } = options; + + if (typeof nonce !== 'undefined') { + nonce = parseInt(nonce); + if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.'); + } + + if (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(channel.client.resolver.resolveString(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); + } + + // Add the reply prefix + if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { + const id = channel.client.resolver.resolveUserID(reply); + const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; + if (split) split.prepend = `${mention}, ${split.prepend || ''}`; + content = `${mention}${typeof content !== 'undefined' ? `, ${content}` : ''}`; + } + + 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); +}; diff --git a/src/structures/shared/index.js b/src/structures/shared/index.js new file mode 100644 index 000000000..67eed7f83 --- /dev/null +++ b/src/structures/shared/index.js @@ -0,0 +1,4 @@ +module.exports = { + search: require('./Search'), + sendMessage: require('./SendMessage'), +}; diff --git a/src/util/Constants.js b/src/util/Constants.js index 3678410ac..5e5c2c1ee 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -106,101 +106,7 @@ const AllowedImageSizes = [ 2048, ]; -const Endpoints = exports.Endpoints = { - User: userID => { - if (userID.id) userID = userID.id; - const base = `/users/${userID}`; - return { - toString: () => base, - channels: `${base}/channels`, - profile: `${base}/profile`, - relationships: `${base}/relationships`, - settings: `${base}/settings`, - Relationship: uID => `${base}/relationships/${uID}`, - Guild: guildID => `${base}/guilds/${guildID}`, - Note: id => `${base}/notes/${id}`, - Mentions: (limit, roles, everyone, guildID) => - `${base}/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`, - Avatar: (root, hash, format, size) => { - if (userID === '1') return hash; - return Endpoints.CDN(root).Avatar(userID, hash, format, size); - }, - }; - }, - guilds: '/guilds', - Guild: guildID => { - if (guildID.id) guildID = guildID.id; - const base = `/guilds/${guildID}`; - return { - toString: () => base, - prune: `${base}/prune`, - embed: `${base}/embed`, - bans: `${base}/bans`, - integrations: `${base}/integrations`, - members: `${base}/members`, - channels: `${base}/channels`, - invites: `${base}/invites`, - roles: `${base}/roles`, - emojis: `${base}/emojis`, - search: `${base}/messages/search`, - voiceRegions: `${base}/regions`, - webhooks: `${base}/webhooks`, - ack: `${base}/ack`, - settings: `${base}/settings`, - auditLogs: `${base}/audit-logs`, - Emoji: emojiID => `${base}/emojis/${emojiID}`, - Icon: (root, hash, format, size) => Endpoints.CDN(root).Icon(guildID, hash, format, size), - Splash: (root, hash) => Endpoints.CDN(root).Splash(guildID, hash), - Role: roleID => `${base}/roles/${roleID}`, - Member: memberID => { - if (memberID.id) memberID = memberID.id; - const mbase = `${base}/members/${memberID}`; - return { - toString: () => mbase, - Role: roleID => `${mbase}/roles/${roleID}`, - nickname: `${base}/members/@me/nick`, - }; - }, - }; - }, - channels: '/channels', - Channel: channelID => { - if (channelID.id) channelID = channelID.id; - const base = `/channels/${channelID}`; - return { - toString: () => base, - messages: { - toString: () => `${base}/messages`, - bulkDelete: `${base}/messages/bulk-delete`, - }, - invites: `${base}/invites`, - typing: `${base}/typing`, - permissions: `${base}/permissions`, - webhooks: `${base}/webhooks`, - search: `${base}/messages/search`, - pins: `${base}/pins`, - Pin: messageID => `${base}/pins/${messageID}`, - Recipient: recipientID => `${base}/recipients/${recipientID}`, - Message: messageID => { - if (messageID.id) messageID = messageID.id; - const mbase = `${base}/messages/${messageID}`; - return { - toString: () => mbase, - reactions: `${mbase}/reactions`, - ack: `${mbase}/ack`, - Reaction: (emoji, limit) => { - const rbase = `${mbase}/reactions/${emoji}${limit ? `?limit=${limit}` : ''}`; - return { - toString: () => rbase, - User: userID => `${rbase}/${userID}`, - }; - }, - }; - }, - }; - }, - Message: m => exports.Endpoints.Channel(m.channel).Message(m), - Member: m => exports.Endpoints.Guild(m.guild).Member(m), +exports.Endpoints = { CDN(root) { return { Emoji: emojiID => `${root}/emojis/${emojiID}.png`, @@ -227,26 +133,7 @@ const Endpoints = exports.Endpoints = { Splash: (guildID, hash) => `${root}/splashes/${guildID}/${hash}.jpg`, }; }, - OAUTH2: { - Application: appID => { - const base = `/oauth2/applications/${appID}`; - return { - toString: () => base, - reset: `${base}/reset`, - }; - }, - App: appID => `/oauth2/authorize?client_id=${appID}`, - }, - login: '/auth/login', - logout: '/auth/logout', - voiceRegions: '/voice/regions', - gateway: { - toString: () => '/gateway', - bot: '/gateway/bot', - }, - Invite: inviteID => `/invite/${inviteID}?with_counts=true`, - inviteLink: id => `https://discord.gg/${id}`, - Webhook: (webhookID, token) => `/webhooks/${webhookID}${token ? `/${token}` : ''}`, + invite: code => `https://discord.gg/${code}`, }; diff --git a/src/util/Util.js b/src/util/Util.js index 9b7d536cd..2e844be01 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -30,7 +30,7 @@ class Util { } messages[msg] += (messages[msg].length > 0 && messages[msg] !== prepend ? char : '') + splitText[i]; } - return messages; + return messages.filter(m => m); } /** diff --git a/test/tester1000.js b/test/tester1000.js new file mode 100644 index 000000000..45ed165d9 --- /dev/null +++ b/test/tester1000.js @@ -0,0 +1,46 @@ +const Discord = require('../src'); +const { token, prefix, owner } = require('./auth.js'); + +// eslint-disable-next-line no-console +const log = (...args) => console.log(process.uptime().toFixed(3), ...args); + +const client = new Discord.Client(); + +client.on('debug', log); +client.on('ready', () => { + log('READY', client.user.tag, client.user.id); +}); + +const commands = { + eval: message => { + if (message.author.id !== owner) return; + let res; + try { + res = eval(message.content); + if (typeof res !== 'string') res = require('util').inspect(res); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err.stack); + res = err.message; + } + message.channel.send(res, { code: 'js' }); + }, +}; + +client.on('message', message => { + if (!message.content.startsWith(prefix) || message.author.bot) return; + + message.content = message.content.replace(prefix, '').trim().split(' '); + const command = message.content.shift(); + message.content = message.content.join(' '); + + // eslint-disable-next-line no-console + console.log('COMMAND', command, message.content); + + if (command in commands) commands[command](message); +}); + +client.login(token); + +// eslint-disable-next-line no-console +process.on('unhandledRejection', console.error); From 874e94992ba0f35c3f7fa98dd16fffdfb989e600 Mon Sep 17 00:00:00 2001 From: vzwGrey Date: Sun, 21 May 2017 18:56:17 +0200 Subject: [PATCH 0048/1359] Fix reaction collector example (#1513) --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 4881fe127..5cb6cb049 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -254,7 +254,7 @@ class Message { * @example * // Create a reaction collector * const collector = message.createReactionCollector( - * (reaction, user) => reaction.emoji.id === '👌' && user.id === 'someID', + * (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID', * { time: 15000 } * ); * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); From ec5da9e6ad8b7a654052d7e48bbe30da1b55fc60 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sun, 21 May 2017 14:12:49 -0500 Subject: [PATCH 0049/1359] =?UTF-8?q?Fix=20trying=20to=20use=20this.client?= =?UTF-8?q?,=20when=20this=20IS=20client,=20and=20this.client=20i=E2=80=A6?= =?UTF-8?q?=20(#1514)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/Client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index c896d00ee..a57a2f04f 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -351,7 +351,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchWebhook(id, token) { - return this.api.webhooks(id, token).get().then(data => new Webhook(this.client, data)); + return this.api.webhooks(id, token).get().then(data => new Webhook(this, data)); } /** @@ -409,7 +409,7 @@ class Client extends EventEmitter { */ fetchApplication(id = '@me') { return this.rest.api.oauth2.applications(id).get() - .then(app => new OAuth2Application(this.client, app)); + .then(app => new OAuth2Application(this, app)); } /** From a0df2c5fa41ecd7f696a0246cf8d8fca798347f3 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 21 May 2017 14:24:36 -0500 Subject: [PATCH 0050/1359] they can be more than just string/num/bool (#1448) --- src/structures/GuildAuditLogs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 163c89d01..8cc95779a 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -172,8 +172,8 @@ class GuildAuditLogsEntry { * An entry in the audit log representing a specific change * @typedef {object} AuditLogChange * @property {string} key The property that was changed, e.g. `nick` for nickname changes - * @property {string|boolean|number} [old] The old value of the change, e.g. for nicknames, the old nickname - * @property {string|boolean|number} [new] The new value of the change, e.g. for nicknames, the new nickname + * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname + * @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname */ /** From fce15ba33c749ae099cf8e9db7d9965f61e293b3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 21 May 2017 22:04:57 +0200 Subject: [PATCH 0051/1359] Fix for bulkDelete, acknowledge, createInvite and remove some redundant stuff (#1515) --- src/client/Client.js | 2 +- src/structures/Guild.js | 2 +- src/structures/GuildChannel.js | 4 ++-- src/structures/interfaces/TextBasedChannel.js | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index a57a2f04f..0fd8c18e6 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -359,7 +359,7 @@ class Client extends EventEmitter { * @returns {Collection} */ fetchVoiceRegions() { - return this.rest.api.voice.regions.get().then(res => { + return this.api.voice.regions.get().then(res => { const regions = new Collection(); for (const region of res) regions.set(region.id, new VoiceRegion(region)); return regions; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 5f1108292..bb9003b15 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -975,7 +975,7 @@ class Guild { */ leave() { if (this.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); - return this.rest.api.users('@me').guilds(this.id).delete() + return this.client.api.users('@me').guilds(this.id).delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 1d5750a3d..81976e987 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -283,10 +283,10 @@ class GuildChannel extends Channel { * kicked after 24 hours if they have not yet received a role * @param {number} [options.maxAge=86400] How long the invite should last (in seconds, 0 for forever) * @param {number} [options.maxUses=0] Maximum number of uses - * @param {string} [reason] Reason for creating this + * @param {string} [options.reason] Reason for creating this * @returns {Promise} */ - createInvite({ temporary = false, maxAge = 86400, maxUses = 0 }, reason) { + createInvite({ temporary = false, maxAge = 86400, maxUses = 0, reason } = {}) { return this.client.api.channels(this.id).invites.post({ data: { temporary, max_age: maxAge, max_uses: maxUses, }, reason }) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 7c3b64fda..da16b9958 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -379,7 +379,7 @@ class TextBasedChannel { Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000 ); } - return this.rest.api.channels(this.id).messages['bulk-delete'] + return this.client.api.channels(this.id).messages()['bulk-delete'] .post({ data: { messages: messageIDs } }) .then(() => this.client.actions.MessageDeleteBulk.handle({ @@ -398,7 +398,7 @@ class TextBasedChannel { */ acknowledge() { if (!this.lastMessageID) return Promise.resolve(this); - return this.client.api.channels(this.id).messages(this.lastMessageID) + return this.client.api.channels(this.id).messages(this.lastMessageID).ack .post({ data: { token: this.client.rest._ackToken } }) .then(res => { if (res.token) this.client.rest._ackToken = res.token; From 694f8278f1373ee562d60eacbbf705cdf3f3f9a7 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 21 May 2017 15:09:37 -0500 Subject: [PATCH 0052/1359] make fetching members way better (#1124) * make fetching members way better * fix up fetching edge cases yet again * stop making useless collections * Update Guild.js * Update Guild.js * consistency? * Update GuildMembersChunk.js * Update Guild.js * Update Guild.js * Open editor to fix issues instead of GH: check --- .../packets/handlers/GuildMembersChunk.js | 11 +++-------- src/structures/Guild.js | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js index 835bcb573..685de80c1 100644 --- a/src/client/websocket/packets/handlers/GuildMembersChunk.js +++ b/src/client/websocket/packets/handlers/GuildMembersChunk.js @@ -1,7 +1,6 @@ const AbstractHandler = require('./AbstractHandler'); const Constants = require('../../../../util/Constants'); -// Uncomment in v12 -// const Collection = require('../../../../util/Collection'); +const Collection = require('../../../../util/Collection'); class GuildMembersChunkHandler extends AbstractHandler { handle(packet) { @@ -9,13 +8,9 @@ class GuildMembersChunkHandler extends AbstractHandler { const data = packet.d; const guild = client.guilds.get(data.guild_id); if (!guild) return; + const members = new Collection(); - // Uncomment in v12 - // const members = new Collection(); - // - // for (const member of data.members) members.set(member.id, guild._addMember(member, false)); - - const members = data.members.map(member => guild._addMember(member, false)); + for (const member of data.members) members.set(member.id, guild._addMember(member, false)); client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members, guild); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index bb9003b15..66b0ddf05 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -500,14 +500,12 @@ class Guild { * this should not be necessary. * @param {string} [query=''] Limit fetch to members with similar usernames * @param {number} [limit=0] Maximum number of members to request - * @returns {Promise} + * @returns {Promise>} */ fetchMembers(query = '', limit = 0) { return new Promise((resolve, reject) => { if (this.memberCount === this.members.size) { - // Uncomment in v12 - // resolve(this.members) - resolve(this); + resolve(new Collection()); return; } this.client.ws.send({ @@ -518,17 +516,20 @@ class Guild { limit, }, }); + const fetchedMembers = new Collection(); const handler = (members, guild) => { if (guild.id !== this.id) return; - if (this.memberCount === this.members.size || members.length < 1000) { + for (const member of members.values()) fetchedMembers.set(member.user.id, member); + if (this.memberCount === this.members.size || ((query || limit) && members.size < 1000)) { this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); - // Uncomment in v12 - // resolve(this.members) - resolve(this); + resolve(fetchedMembers); } }; this.client.on(Constants.Events.GUILD_MEMBERS_CHUNK, handler); - this.client.setTimeout(() => reject(new Error('Members didn\'t arrive in time.')), 120 * 1000); + this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); + reject(new Error('Members didn\'t arrive in time.')); + }, 120 * 1000); }); } From 75eb0bae58ecc636ac4c1778a711905bc84fd8bc Mon Sep 17 00:00:00 2001 From: bdistin Date: Sun, 21 May 2017 18:02:08 -0500 Subject: [PATCH 0053/1359] Add resolve content back into send (#1516) so arrays are joined again, instead of sent as one new message per element --- src/structures/shared/SendMessage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index cfeef8ef9..df60b7b2c 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -12,6 +12,7 @@ module.exports = function sendMessage(channel, options) { } if (content) { + content = channel.client.resolver.resolveString(content); if (split && typeof split !== 'object') split = {}; // Wrap everything in a code block if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { From 05a9f7e72fe0b56f9438ac13749819c4c07dc9cf Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 22 May 2017 01:04:27 +0200 Subject: [PATCH 0054/1359] Remove duplicate resolveString call --- src/structures/shared/SendMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index df60b7b2c..d93c73cff 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -16,7 +16,7 @@ module.exports = function sendMessage(channel, options) { if (split && typeof split !== 'object') split = {}; // Wrap everything in a code block if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { - content = Util.escapeMarkdown(channel.client.resolver.resolveString(content), true); + content = Util.escapeMarkdown(content, true); content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; if (split) { split.prepend = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n`; From 7309e367f1afab2d947794160b6f4665764c39b8 Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 23 May 2017 11:14:25 +0200 Subject: [PATCH 0055/1359] Update typings --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 54b72a2b7..99e19b9d7 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 54b72a2b75abf4f43b2ddd304afaa5c83af51ad2 +Subproject commit 99e19b9d76dde5f1498b58bb9261042d9afa055c From 4292134647608206e2b3b379485a5494854c648e Mon Sep 17 00:00:00 2001 From: Drahcirius Date: Tue, 23 May 2017 06:18:18 -0400 Subject: [PATCH 0056/1359] Refactored static resolvers to Util (#1517) * ready event will now throw errors properly * ws login rejection fix * moved static resolves to util --- src/client/ClientDataResolver.js | 90 +--------------------------- src/structures/Guild.js | 2 +- src/structures/Message.js | 4 +- src/structures/RichEmbed.js | 22 +++---- src/structures/Role.js | 3 +- src/structures/Webhook.js | 3 +- src/structures/shared/SendMessage.js | 2 +- src/util/Util.js | 79 ++++++++++++++++++++++++ 8 files changed, 97 insertions(+), 108 deletions(-) diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index 04d6b0b01..1c268b73d 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -2,8 +2,7 @@ const path = require('path'); const fs = require('fs'); const snekfetch = require('snekfetch'); -const Constants = require('../util/Constants'); -const convertToBuffer = require('../util/Util').convertToBuffer; +const Util = require('../util/Util'); const User = require('../structures/User'); const Message = require('../structures/Message'); const Guild = require('../structures/Guild'); @@ -155,25 +154,6 @@ class ClientDataResolver { return data; } - /** - * Data that can be resolved to give a string. This can be: - * * A string - * * An array (joined with a new line delimiter to give a string) - * * Any value - * @typedef {string|Array|*} StringResolvable - */ - - /** - * Resolves a StringResolvable to a string. - * @param {StringResolvable} data The string resolvable to resolve - * @returns {string} - */ - resolveString(data) { - if (typeof data === 'string') return data; - if (data instanceof Array) return data.join('\n'); - return String(data); - } - /** * Data that resolves to give a Base64 string, typically for image uploading. This can be: * * A Buffer @@ -206,7 +186,7 @@ class ClientDataResolver { */ resolveBuffer(resource) { if (resource instanceof Buffer) return Promise.resolve(resource); - if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertToBuffer(resource)); + if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(Util.convertToBuffer(resource)); if (typeof resource === 'string') { return new Promise((resolve, reject) => { @@ -257,72 +237,6 @@ 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', - * 'RANDOM', - * ] - * ``` - * or something like - * ``` - * [255, 0, 255] - * ``` - * for purple - * @typedef {string|number|Array} ColorResolvable - */ - - /** - * Resolves a ColorResolvable into a color number. - * @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)); - 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 this.constructor.resolveColor(color); - } } module.exports = ClientDataResolver; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 66b0ddf05..23a697940 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -911,7 +911,7 @@ class Guild { * .catch(console.error) */ createRole({ data = {}, reason } = {}) { - if (data.color) data.color = this.client.resolver.resolveColor(data.color); + if (data.color) data.color = Util.resolveColor(data.color); if (data.permissions) data.permissions = Permissions.resolve(data.permissions); return this.client.api.guilds(this.id).roles.post({ data, reason }).then(role => diff --git a/src/structures/Message.js b/src/structures/Message.js index 5cb6cb049..f886b13a2 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -381,13 +381,13 @@ class Message { options = {}; } - if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + if (typeof content !== 'undefined') content = Util.resolveString(content); const { embed, code, reply } = options; // Wrap everything in a code block if (typeof code !== 'undefined' && (typeof code !== 'boolean' || code === true)) { - content = Util.escapeMarkdown(this.client.resolver.resolveString(content), true); + content = Util.escapeMarkdown(Util.resolveString(content), true); content = `\`\`\`${typeof code !== 'boolean' ? code || '' : ''}\n${content}\n\`\`\``; } diff --git a/src/structures/RichEmbed.js b/src/structures/RichEmbed.js index 62b40e185..014943130 100644 --- a/src/structures/RichEmbed.js +++ b/src/structures/RichEmbed.js @@ -1,4 +1,4 @@ -const ClientDataResolver = require('../client/ClientDataResolver'); +const Util = require('../util/Util'); /** * A rich embed to be sent with a message with a fluent interface for creation. @@ -79,7 +79,7 @@ class RichEmbed { * @returns {RichEmbed} This embed */ setTitle(title) { - title = resolveString(title); + title = Util.resolveString(title); if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.'); this.title = title; return this; @@ -91,7 +91,7 @@ class RichEmbed { * @returns {RichEmbed} This embed */ setDescription(description) { - description = resolveString(description); + description = Util.resolveString(description); if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.'); this.description = description; return this; @@ -113,7 +113,7 @@ class RichEmbed { * @returns {RichEmbed} This embed */ setColor(color) { - this.color = ClientDataResolver.resolveColor(color); + this.color = Util.resolveColor(color); return this; } @@ -125,7 +125,7 @@ class RichEmbed { * @returns {RichEmbed} This embed */ setAuthor(name, icon, url) { - this.author = { name: resolveString(name), icon_url: icon, url }; + this.author = { name: Util.resolveString(name), icon_url: icon, url }; return this; } @@ -148,10 +148,10 @@ class RichEmbed { */ addField(name, value, inline = false) { if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.'); - name = resolveString(name); + name = Util.resolveString(name); if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); if (!/\S/.test(name)) throw new RangeError('RichEmbed field names may not be empty.'); - value = resolveString(value); + value = Util.resolveString(value); if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); if (!/\S/.test(value)) throw new RangeError('RichEmbed field values may not be empty.'); this.fields.push({ name, value, inline }); @@ -194,7 +194,7 @@ class RichEmbed { * @returns {RichEmbed} This embed */ setFooter(text, icon) { - text = resolveString(text); + text = Util.resolveString(text); if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.'); this.footer = { text, icon_url: icon }; return this; @@ -214,9 +214,3 @@ class RichEmbed { } module.exports = RichEmbed; - -function resolveString(data) { - if (typeof data === 'string') return data; - if (data instanceof Array) return data.join('\n'); - return String(data); -} diff --git a/src/structures/Role.js b/src/structures/Role.js index 40d997fe7..331f16b4f 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -1,5 +1,6 @@ const Snowflake = require('../util/Snowflake'); const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); /** * Represents a role on Discord. @@ -205,7 +206,7 @@ class Role { data: { name: data.name || this.name, position: typeof data.position !== 'undefined' ? data.position : this.position, - color: this.client.resolver.resolveColor(data.color || this.color), + color: Util.resolveColor(data.color || this.color), hoist: typeof data.hoist !== 'undefined' ? data.hoist : this.hoist, mentionable: typeof data.mentionable !== 'undefined' ? data.mentionable : this.mentionable, }, diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 3a6bc5b6c..bf9b46a2c 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,4 +1,5 @@ const path = require('path'); +const Util = require('../util/Util'); /** * Represents a webhook. @@ -113,7 +114,7 @@ class Webhook { options.avatarURL = null; } - if (typeof content !== 'undefined') content = this.client.resolver.resolveString(content); + if (typeof content !== 'undefined') content = Util.resolveString(content); if (content) { if (options.disableEveryone || (typeof options.disableEveryone === 'undefined' && this.client.options.disableEveryone) diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index d93c73cff..b0c7a3b12 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -12,7 +12,7 @@ module.exports = function sendMessage(channel, options) { } if (content) { - content = channel.client.resolver.resolveString(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)) { diff --git a/src/util/Util.js b/src/util/Util.js index 2e844be01..0cf2e1130 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -207,6 +207,85 @@ class Util { } return array.indexOf(element); } + + /** + * Data that can be resolved to give a string. This can be: + * * A string + * * An array (joined with a new line delimiter to give a string) + * * Any value + * @typedef {string|Array|*} StringResolvable + */ + + /** + * Resolves a StringResolvable to a string. + * @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'); + return String(data); + } + + /** + * 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', + * 'RANDOM', + * ] + * ``` + * or something like + * ``` + * [255, 0, 255] + * ``` + * for purple + * @typedef {string|number|Array} ColorResolvable + */ + + /** + * Resolves a ColorResolvable into a color number. + * @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)); + 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; + } } module.exports = Util; From ee679c732095be43378ce333680f4e68af6b239d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 27 May 2017 19:19:35 +0100 Subject: [PATCH 0057/1359] Fix #1528 --- src/client/websocket/WebSocketConnection.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index 93560a45b..af4a20e8d 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -359,13 +359,16 @@ class WebSocketConnection extends EventEmitter { * @param {Error} error Error that occurred */ onError(error) { + if (error && error.message === 'uWs client connection error') { + this.reconnect(); + return; + } /** * Emitted whenever the client's WebSocket encounters a connection error. * @event Client#error * @param {Error} error The encountered error */ this.client.emit(Constants.Events.ERROR, error); - if (error.message === 'uWs client connection error') this.reconnect(); } /** From afe0bd8bc4f7810fe964fb3b6620a36098d6c011 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 27 May 2017 22:04:19 +0200 Subject: [PATCH 0058/1359] User#defaultAvatarURL should use CDN (#1531) --- src/structures/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/User.js b/src/structures/User.js index 2797f7ff9..08056184c 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -125,7 +125,7 @@ class User { * @readonly */ get defaultAvatarURL() { - return Constants.Endpoints.CDN(this.client.options.http.host).DefaultAvatar(this.discriminator % 5); + return Constants.Endpoints.CDN(this.client.options.http.cdn).DefaultAvatar(this.discriminator % 5); } /** From 555317043e53103718bad1e47a6cf3757982beca Mon Sep 17 00:00:00 2001 From: Drahcirius Date: Sat, 27 May 2017 21:34:30 -0400 Subject: [PATCH 0059/1359] Message embeds (#1529) * wowe * fix sending * Update MessageEmbed.js * lel * patched some fields for message embed and transforms edits as well * webhook embeds transform * apply transform to webhook embeds, and changed references * Update MessageEmbed.js * Update ClientDataResolver.js * updated embeds params and use new util resolvers * did not mean to add this back * use master version of ClientDataResolver * transform no longer needed --- src/structures/Message.js | 4 +- src/structures/MessageEmbed.js | 525 ++++++++++++++------------------- 2 files changed, 218 insertions(+), 311 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index f886b13a2..e87c33434 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -91,7 +91,7 @@ class Message { * A list of embeds in the message - e.g. YouTube Player * @type {MessageEmbed[]} */ - this.embeds = data.embeds.map(e => new Embed(this, e)); + this.embeds = data.embeds.map(e => new Embed(e)); /** * A collection of attachments in the message - e.g. Pictures - mapped by their ID @@ -163,7 +163,7 @@ class Message { if ('content' in data) this.content = data.content; if ('pinned' in data) this.pinned = data.pinned; if ('tts' in data) this.tts = data.tts; - if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(this, e)); + if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(e)); else this.embeds = this.embeds.slice(); if ('attachments' in data) { diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index c15a90ffa..35d222b1c 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -1,27 +1,14 @@ +const Util = require('../util/Util'); + /** * Represents an embed in a message (image/video preview, rich embed, etc.) - * This class is only used for *recieved* embeds. If you wish to send one, use the {@link RichEmbed} class. */ class MessageEmbed { - constructor(message, data) { - /** - * The client that instantiated this embed - * @name MessageEmbed#client - * @type {Client} - * @readonly - */ - Object.defineProperty(this, 'client', { value: message.client }); - - /** - * The message this embed is part of - * @type {Message} - */ - this.message = message; - + constructor(data) { this.setup(data); } - setup(data) { + setup(data) { // eslint-disable-line complexity /** * The type of this embed * @type {string} @@ -32,74 +19,122 @@ class MessageEmbed { * The title of this embed * @type {?string} */ - this.title = data.title; + this.title = data.title || null; /** * The description of this embed * @type {?string} */ - this.description = data.description; + this.description = data.description || null; /** * The URL of this embed - * @type {string} + * @type {?string} */ - this.url = data.url; + this.url = data.url || null; /** * The color of the embed - * @type {number} + * @type {?number} */ - this.color = data.color; - - /** - * The fields of this embed - * @type {MessageEmbedField[]} - */ - this.fields = []; - if (data.fields) for (const field of data.fields) this.fields.push(new MessageEmbedField(this, field)); + this.color = data.color || null; /** * The timestamp of this embed - * @type {number} + * @type {?number} */ - this.createdTimestamp = data.timestamp; + this.timestamp = new Date(data.timestamp) || null; /** - * The thumbnail of this embed - * @type {?MessageEmbedThumbnail} + * The fields of this embed + * @type {?Object[]} + * @property {string} name The name of this field + * @property {string} value The value of this field + * @property {boolean} inline If this field will be displayed inline */ - this.thumbnail = data.thumbnail ? new MessageEmbedThumbnail(this, data.thumbnail) : null; + this.fields = data.fields || null; /** - * The image of this embed - * @type {?MessageEmbedImage} + * The thumbnail of this embed, if there is one + * @type {?Object} + * @property {string} url URL for this thumbnail + * @property {string} proxyURL ProxyURL for this thumbnail + * @property {number} height Height of this thumbnail + * @property {number} width Width of this thumbnail */ - this.image = data.image ? new MessageEmbedImage(this, data.image) : null; + this.thumbnail = data.thumbnail ? { + url: data.thumbnail.url || null, + proxyURL: data.thumbnail.proxy_url, + height: data.height, + width: data.width, + } : null; /** - * The video of this embed - * @type {?MessageEmbedVideo} + * The image of this embed, if there is one + * @type {?Object} + * @property {string} url URL for this image + * @property {string} proxyURL ProxyURL for this image + * @property {number} height Height of this image + * @property {number} width Width of this image */ - this.video = data.video ? new MessageEmbedVideo(this, data.video) : null; + this.image = data.image ? { + url: data.image.url || null, + proxyURL: data.image.proxy_url, + height: data.height, + width: data.width, + } : null; /** - * The author of this embed - * @type {?MessageEmbedAuthor} + * The video of this embed, if there is one + * @type {?Object} + * @property {string} url URL of this video + * @property {number} height Height of this video + * @property {number} width Width of this video */ - this.author = data.author ? new MessageEmbedAuthor(this, data.author) : null; + this.video = data.video ? { + url: data.video.url || null, + height: data.video.height, + width: data.video.width, + } : null; /** - * The provider of this embed - * @type {?MessageEmbedProvider} + * The author of this embed, if there is one + * @type {?Object} + * @property {string} name The name of this author + * @property {string} url URL of this author + * @property {string} iconURL URL of the icon for this author + * @property {string} proxyIconURL Proxied URL of the icon for this author */ - this.provider = data.provider ? new MessageEmbedProvider(this, data.provider) : null; + this.author = data.author ? { + name: data.author.name || null, + url: data.author.url || null, + iconURL: data.author.iconURL || data.author.icon_url || null, + proxyIconURL: data.author.proxyIconUrl || data.author.proxy_icon_url || null, + } : null; + + /** + * The provider of this embed, if there is one + * @type {?Object} + * @property {string} name The name of this provider + * @property {string} url URL of this provider + */ + this.provider = data.provider ? { + name: data.provider.name, + url: data.provider.url, + } : null; /** * The footer of this embed - * @type {?MessageEmbedFooter} + * @type {?Object} + * @property {string} text The text of this footer + * @property {string} iconURL URL of the icon for this footer + * @property {string} proxyIconURL Proxied URL of the icon for this footer */ - this.footer = data.footer ? new MessageEmbedFooter(this, data.footer) : null; + this.footer = data.footer ? { + text: data.footer.text || null, + iconURL: data.footer.iconURL || data.footer.icon_url || null, + proxyIconURL: data.footer.proxyIconURL || data.footer.proxy_icon_url || null, + } : null; } /** @@ -108,7 +143,7 @@ class MessageEmbed { * @readonly */ get createdAt() { - return new Date(this.createdTimestamp); + return new Date(this.timestamp); } /** @@ -121,265 +156,137 @@ class MessageEmbed { while (col.length < 6) col = `0${col}`; return `#${col}`; } + + /** + * Adds a field to the embed (max 25). + * @param {StringResolvable} name The name of the field + * @param {StringResolvable} value The value of the field + * @param {boolean} [inline=false] Set the field to display inline + * @returns {MessageEmbed} This embed + */ + addField(name, value, inline = false) { + if (this.fields.length >= 25) throw new RangeError('MessageEmbeds may not exceed 25 fields.'); + name = Util.resolveString(name); + if (name.length > 256) throw new RangeError('MessageEmbed field names may not exceed 256 characters.'); + if (!/\S/.test(name)) throw new RangeError('MessageEmbed field names may not be empty.'); + value = Util.resolveString(value); + if (value.length > 1024) throw new RangeError('MessageEmbed field values may not exceed 1024 characters.'); + if (!/\S/.test(value)) throw new RangeError('MessageEmbed field values may not be empty.'); + this.fields.push({ name, value, inline }); + return this; + } + + /** + * Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when + * setting an embed image or author/footer icons. Only one file may be attached. + * @param {FileOptions|string} file Local path or URL to the file to attach, or valid FileOptions for a file to attach + * @returns {MessageEmbed} This embed + */ + attachFile(file) { + if (this.file) throw new RangeError('You may not upload more than one file at once.'); + this.file = file; + return this; + } + + /** + * Sets the author of this embed. + * @param {StringResolvable} name The name of the author + * @param {string} [iconURL] The icon URL of the author + * @param {string} [url] The URL of the author + * @returns {MessageEmbed} This embed + */ + setAuthor(name, iconURL, url) { + this.author = { name: Util.resolveString(name), iconURL, url }; + return this; + } + + /** + * Sets the color of this embed. + * @param {ColorResolvable} color The color of the embed + * @returns {MessageEmbed} This embed + */ + setColor(color) { + this.color = Util.resolveColor(color); + return this; + } + + /** + * Sets the description of this embed. + * @param {StringResolvable} description The description + * @returns {MessageEmbed} This embed + */ + setDescription(description) { + description = Util.resolveString(description); + if (description.length > 2048) throw new RangeError('MessageEmbed descriptions may not exceed 2048 characters.'); + this.description = description; + return this; + } + + /** + * Sets the footer of this embed. + * @param {StringResolvable} text The text of the footer + * @param {string} [iconURL] The icon URL of the footer + * @returns {MessageEmbed} This embed + */ + setFooter(text, iconURL) { + text = Util.resolveString(text); + if (text.length > 2048) throw new RangeError('MessageEmbed footer text may not exceed 2048 characters.'); + this.footer = { text, iconURL }; + return this; + } + + /** + * Set the image of this embed. + * @param {string} url The URL of the image + * @returns {MessageEmbed} This embed + */ + setImage(url) { + this.image = { url }; + return this; + } + + /** + * Set the thumbnail of this embed. + * @param {string} url The URL of the thumbnail + * @returns {MessageEmbed} This embed + */ + setThumbnail(url) { + this.thumbnail = { url }; + return this; + } + + /** + * Sets the timestamp of this embed. + * @param {Date} [timestamp=current date] The timestamp + * @returns {MessageEmbed} This embed + */ + setTimestamp(timestamp = new Date()) { + this.timestamp = timestamp; + return this; + } + + /** + * Sets the title of this embed. + * @param {StringResolvable} title The title + * @returns {MessageEmbed} This embed + */ + setTitle(title) { + title = Util.resolveString(title); + if (title.length > 256) throw new RangeError('MessageEmbed titles may not exceed 256 characters.'); + this.title = title; + return this; + } + + /** + * Sets the URL of this embed. + * @param {string} url The URL + * @returns {MessageEmbed} This embed + */ + setURL(url) { + this.url = url; + return this; + } + } -/** - * Represents a thumbnail for a message embed. - */ -class MessageEmbedThumbnail { - constructor(embed, data) { - /** - * The embed this thumbnail is part of - * @type {MessageEmbed} - */ - this.embed = embed; - - this.setup(data); - } - - setup(data) { - /** - * The URL for this thumbnail - * @type {string} - */ - this.url = data.url; - - /** - * The Proxy URL for this thumbnail - * @type {string} - */ - this.proxyURL = data.proxy_url; - - /** - * The height of the thumbnail - * @type {number} - */ - this.height = data.height; - - /** - * The width of the thumbnail - * @type {number} - */ - this.width = data.width; - } -} - -/** - * Represents an image for a message embed. - */ -class MessageEmbedImage { - constructor(embed, data) { - /** - * The embed this image is part of - * @type {MessageEmbed} - */ - this.embed = embed; - - this.setup(data); - } - - setup(data) { - /** - * The URL for this image - * @type {string} - */ - this.url = data.url; - - /** - * The Proxy URL for this image - * @type {string} - */ - this.proxyURL = data.proxy_url; - - /** - * The height of the image - * @type {number} - */ - this.height = data.height; - - /** - * The width of the image - * @type {number} - */ - this.width = data.width; - } -} - -/** - * Represents a video for a message embed. - */ -class MessageEmbedVideo { - constructor(embed, data) { - /** - * The embed this video is part of - * @type {MessageEmbed} - */ - this.embed = embed; - - this.setup(data); - } - - setup(data) { - /** - * The source URL for this video - * @type {string} - */ - this.url = data.url; - - /** - * The height of the video - * @type {number} - */ - this.height = data.height; - - /** - * The width of the video - * @type {number} - */ - this.width = data.width; - } -} - -/** - * Represents a provider for a message embed. - */ -class MessageEmbedProvider { - constructor(embed, data) { - /** - * The embed this provider is part of - * @type {MessageEmbed} - */ - this.embed = embed; - - this.setup(data); - } - - setup(data) { - /** - * The name of this provider - * @type {string} - */ - this.name = data.name; - - /** - * The URL of this provider - * @type {string} - */ - this.url = data.url; - } -} - -/** - * Represents an author for a message embed. - */ -class MessageEmbedAuthor { - constructor(embed, data) { - /** - * The embed this author is part of - * @type {MessageEmbed} - */ - this.embed = embed; - - this.setup(data); - } - - setup(data) { - /** - * The name of this author - * @type {string} - */ - this.name = data.name; - - /** - * The URL of this author - * @type {string} - */ - this.url = data.url; - - /** - * The icon URL of this author - * @type {string} - */ - this.iconURL = data.icon_url; - } -} - -/** - * Represents a field for a message embed. - */ -class MessageEmbedField { - constructor(embed, data) { - /** - * The embed this footer is part of - * @type {MessageEmbed} - */ - this.embed = embed; - - this.setup(data); - } - - setup(data) { - /** - * The name of this field - * @type {string} - */ - this.name = data.name; - - /** - * The value of this field - * @type {string} - */ - this.value = data.value; - - /** - * If this field is displayed inline - * @type {boolean} - */ - this.inline = data.inline; - } -} - -/** - * Represents the footer of a message embed. - */ -class MessageEmbedFooter { - constructor(embed, data) { - /** - * The embed this footer is part of - * @type {MessageEmbed} - */ - this.embed = embed; - - this.setup(data); - } - - setup(data) { - /** - * The text in this footer - * @type {string} - */ - this.text = data.text; - - /** - * The icon URL of this footer - * @type {string} - */ - this.iconURL = data.icon_url; - - /** - * The proxy icon URL of this footer - * @type {string} - */ - this.proxyIconUrl = data.proxy_icon_url; - } -} - -MessageEmbed.Thumbnail = MessageEmbedThumbnail; -MessageEmbed.Image = MessageEmbedImage; -MessageEmbed.Video = MessageEmbedVideo; -MessageEmbed.Provider = MessageEmbedProvider; -MessageEmbed.Author = MessageEmbedAuthor; -MessageEmbed.Field = MessageEmbedField; -MessageEmbed.Footer = MessageEmbedFooter; - module.exports = MessageEmbed; From 22e8237bf1644b1dd83aeeb4909a870f2a53b284 Mon Sep 17 00:00:00 2001 From: PhoenixShay Date: Sun, 28 May 2017 16:54:56 -0400 Subject: [PATCH 0060/1359] Fix Util.js : fetchRecommendedShards() (#1532) * Fix Util.js : fetchRecommendedShards() I'm not sure who thought this was a good idea, but by removing gateway, it broke fetchRecommendedShards() in Util.js as it uses ${Constants.Endpoints.gateway.bot}. * Update Constants.js * Update Constants.js * Update Util.js * Update Constants.js --- src/util/Constants.js | 1 + src/util/Util.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 5e5c2c1ee..fdcc78d7b 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -134,6 +134,7 @@ exports.Endpoints = { }; }, invite: code => `https://discord.gg/${code}`, + botGateway: '/gateway/bot', }; diff --git a/src/util/Util.js b/src/util/Util.js index 0cf2e1130..e48d736c8 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -55,7 +55,7 @@ class Util { static fetchRecommendedShards(token, guildsPerShard = 1000) { return new Promise((resolve, reject) => { if (!token) throw new Error('A token must be provided.'); - snekfetch.get(`${ConstantsHttp.host}/api/v${ConstantsHttp.version}${Constants.Endpoints.gateway.bot}`) + snekfetch.get(`${ConstantsHttp.host}/api/v${ConstantsHttp.version}${Constants.Endpoints.botGateway}`) .set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`) .end((err, res) => { if (err) reject(err); From 1aa2293ebf7f2198a69fcc0daceda7ed16faea63 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 29 May 2017 08:19:31 -0500 Subject: [PATCH 0061/1359] add nsfw option to search (#1534) * Update Search.js * Update TextBasedChannel.js * Update Search.js --- src/structures/interfaces/TextBasedChannel.js | 27 ----------------- src/structures/shared/Search.js | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index da16b9958..f76350299 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -199,33 +199,6 @@ class TextBasedChannel { }); } - /** - * @typedef {Object} MessageSearchOptions - * @property {string} [content] Message content - * @property {Snowflake} [maxID] Maximum ID for the filter - * @property {Snowflake} [minID] Minimum ID for the filter - * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, - * or add `-` to negate (e.g. `-file`) - * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) - * @property {UserResolvable} [author] Author to limit search - * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) - * @property {string} [sortBy='recent'] `recent` or `relevant` - * @property {string} [sortOrder='desc'] `asc` or `desc` - * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) - * @property {number} [limit=25] Maximum number of results to get (1 to 25) - * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) - * @property {UserResolvable} [mentions] Mentioned user filter - * @property {boolean} [mentionsEveryone] If everyone is mentioned - * @property {string} [linkHostname] Filter links by hostname - * @property {string} [embedProvider] The name of an embed provider - * @property {string} [embedType] one of `image`, `video`, `url`, `rich` - * @property {string} [attachmentFilename] The name of an attachment - * @property {string} [attachmentExtension] The extension of an attachment - * @property {Date} [before] Date to find messages before - * @property {Date} [after] Date to find messages before - * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) - */ - /** * Performs a search within the channel. * This is only available when using a user account. diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index fc4b20c3c..ce2281d7a 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -1,5 +1,33 @@ const long = require('long'); +/** + * @typedef {Object} MessageSearchOptions + * @property {string} [content] Message content + * @property {Snowflake} [maxID] Maximum ID for the filter + * @property {Snowflake} [minID] Minimum ID for the filter + * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, + * or add `-` to negate (e.g. `-file`) + * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) + * @property {UserResolvable} [author] Author to limit search + * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) + * @property {string} [sortBy='recent'] `recent` or `relevant` + * @property {string} [sortOrder='desc'] `asc` or `desc` + * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) + * @property {number} [limit=25] Maximum number of results to get (1 to 25) + * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) + * @property {UserResolvable} [mentions] Mentioned user filter + * @property {boolean} [mentionsEveryone] If everyone is mentioned + * @property {string} [linkHostname] Filter links by hostname + * @property {string} [embedProvider] The name of an embed provider + * @property {string} [embedType] one of `image`, `video`, `url`, `rich` + * @property {string} [attachmentFilename] The name of an attachment + * @property {string} [attachmentExtension] The extension of an attachment + * @property {Date} [before] Date to find messages before + * @property {Date} [after] Date to find messages before + * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) + * @property {boolean} [nsfw=false] Include results from NSFW channels + */ + module.exports = function search(target, options) { if (typeof options === 'string') options = { content: options }; if (options.before) { @@ -39,6 +67,7 @@ module.exports = function search(target, options) { embed_type: options.embedType, attachment_filename: options.attachmentFilename, attachment_extension: options.attachmentExtension, + include_nsfw: options.nsfw, }; // Lazy load these because some of them use util From 20a9e4a0f8d25a9dba34e9c2fccb5e3698510c32 Mon Sep 17 00:00:00 2001 From: Drahcirius Date: Mon, 29 May 2017 09:19:42 -0400 Subject: [PATCH 0062/1359] .send() not working on members (#1539) --- src/structures/shared/SendMessage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index b0c7a3b12..bd93e4dbe 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -2,8 +2,8 @@ const Util = require('../../util/Util'); module.exports = function sendMessage(channel, options) { const User = require('../User'); - if (channel instanceof User) return channel.createDM().then(dm => dm.send(options)); 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 (typeof nonce !== 'undefined') { From 29ead897d6960032129e94222dede1fb077027d4 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Mon, 29 May 2017 16:53:23 -0500 Subject: [PATCH 0063/1359] Add MEMBER_ROLE_UPDATE to returning 'UPDATE' array (#1538) (#1542) --- src/structures/GuildAuditLogs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 8cc95779a..9dd326637 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -122,6 +122,7 @@ class GuildAuditLogs { Actions.CHANNEL_UPDATE, Actions.CHANNEL_OVERWRITE_UPDATE, Actions.MEMBER_UPDATE, + Actions.MEMBER_ROLE_UPDATE, Actions.ROLE_UPDATE, Actions.INVITE_UPDATE, Actions.WEBHOOK_UPDATE, From 8f633686965640e9d5806a050e5bc0e63ee8034c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 30 May 2017 12:21:37 +0100 Subject: [PATCH 0064/1359] Correct documentation for VoiceConnection (see #1536) --- src/client/voice/VoiceConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 36727e7e8..b2d91722c 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -8,7 +8,7 @@ const EventEmitter = require('events').EventEmitter; const Prism = require('prism-media'); /** - * Represents a connection to a voice channel in Discord. + * Represents a connection a guild's voice server. * ```js * // Obtained using: * voiceChannel.join().then(connection => { From 6cc74d22ff12dd4a1ae18ec882dd46548c44de53 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 30 May 2017 12:22:40 +0100 Subject: [PATCH 0065/1359] fml --- src/client/voice/VoiceConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b2d91722c..ceab3cf14 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -8,7 +8,7 @@ const EventEmitter = require('events').EventEmitter; const Prism = require('prism-media'); /** - * Represents a connection a guild's voice server. + * Represents a connection to a guild's voice server. * ```js * // Obtained using: * voiceChannel.join().then(connection => { From 535a64053495033335abbd6075842f8b0a5db32d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 30 May 2017 12:51:31 +0100 Subject: [PATCH 0066/1359] Update ws and tweetnacl dependencies to new major versions --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0d1d82822..698635a26 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "long": "^3.2.0", "prism-media": "^0.0.1", "snekfetch": "^3.1.0", - "tweetnacl": "^0.14.0", - "ws": "^2.0.0" + "tweetnacl": "^1.0.0", + "ws": "^3.0.0" }, "peerDependencies": { "bufferutil": "^3.0.0", From 68c25fef282365c2916346fea3f580314e45b0a6 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 30 May 2017 08:34:06 -0500 Subject: [PATCH 0067/1359] Update Guild.js (#1545) --- src/structures/Guild.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 23a697940..61c3d5d25 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -373,7 +373,8 @@ class Guild { /** * Fetch a collection of banned users in this guild. - * @returns {Promise>} + * The returned collection contains user objects keyed under `user` and reasons keyed under `reason`. + * @returns {Promise>} */ fetchBans() { return this.client.api.guilds(this.id).bans.get().then(bans => From 7896081966728fd9c1bf833a24690ab630874c9e Mon Sep 17 00:00:00 2001 From: Tyler Date: Tue, 30 May 2017 08:36:04 -0500 Subject: [PATCH 0068/1359] Fix .editable (#1547) --- src/structures/Role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index 331f16b4f..8378975d8 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -120,7 +120,7 @@ class Role { get editable() { if (this.managed) return false; const clientMember = this.guild.member(this.client.user); - if (!clientMember.permissions.has(Permissions.FLAGS.MANAGE_ROLES_OR_PERMISSIONS)) return false; + if (!clientMember.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return false; return clientMember.highestRole.comparePositionTo(this) > 0; } From 47029feafad3f66dc9617dfe8f26a9f91b51c03e Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 30 May 2017 21:25:53 +0200 Subject: [PATCH 0069/1359] Bump to node 8 --- .eslintrc.json | 2 +- .travis.yml | 1 + README.md | 2 +- docs/general/faq.md | 2 +- docs/general/welcome.md | 5 ++--- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 49d819f4b..3d157716f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": "eslint:recommended", "parserOptions": { - "ecmaVersion": 6 + "ecmaVersion": 2017 }, "env": { "es6": true, diff --git a/.travis.yml b/.travis.yml index 74323ddbf..7c558aaba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: node_js node_js: - "6" - "7" + - "8" cache: directories: - node_modules diff --git a/README.md b/README.md index 1bf3f39ac..80253e7c1 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 6.0.0 or newer is required.** +**Node.js 8.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discord.js --save` diff --git a/docs/general/faq.md b/docs/general/faq.md index d7e4188b8..2ec9493be 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -4,7 +4,7 @@ If you have issues not listed here, please ask in the [official Discord server]( Always make sure to read the documentation. ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ -Update to Node.js 6.0.0 or newer. +Update to Node.js 8.0.0 or newer. ## How do I get voice working? - Install FFMPEG. diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 01a3d6717..7cd79baeb 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -17,8 +17,7 @@ # Welcome! -Welcome to the discord.js v11.1.0 documentation. -v11.1.0 features improved voice and gateway stability, as well as support for new features such as audit logs and searching for messages. +Welcome to the discord.js v12.0.0 documentation. ## About discord.js is a powerful [node.js](https://nodejs.org) module that allows you to interact with the @@ -30,7 +29,7 @@ discord.js is a powerful [node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 6.0.0 or newer is required.** +**Node.js 8.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discord.js --save` From 980b58d4df8d2d513ae66c437dd5b38fd0a81fec Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 30 May 2017 23:32:54 +0100 Subject: [PATCH 0070/1359] Ignore package-lock.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c6e423898..b4bdf8b6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Packages node_modules/ yarn.lock +package-lock.json # Log files logs/ From c84529c10262dde20ee8d3315fbc1891ed4f35c4 Mon Sep 17 00:00:00 2001 From: Marko Kajzer Date: Thu, 1 Jun 2017 13:06:46 +0900 Subject: [PATCH 0071/1359] Fixes #1548 (#1551) * Added possibility to remove avatar * Changed as requested * Removed extra blank line --- src/structures/ClientUser.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 95d254054..beb624623 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -82,7 +82,8 @@ class ClientUser extends User { edit(data, password) { const _data = {}; _data.username = data.username || this.username; - _data.avatar = this.client.resolver.resolveBase64(data.avatar) || this.avatar; + _data.avatar = this.client.resolver.resolveBase64(data.avatar); + if (!this.bot) { _data.email = data.email || this.email; _data.password = password; @@ -156,7 +157,7 @@ class ClientUser extends User { if (typeof avatar === 'string' && avatar.startsWith('data:')) { return this.edit({ avatar }); } else { - return this.client.resolver.resolveBuffer(avatar) + return this.client.resolver.resolveBuffer(avatar || Buffer.alloc(0)) .then(data => this.edit({ avatar: this.client.resolver.resolveBase64(data) || null })); } } From c4a7ce12e60e2c1db522788866a99062848f37fe Mon Sep 17 00:00:00 2001 From: aemino Date: Thu, 1 Jun 2017 01:29:55 -0700 Subject: [PATCH 0072/1359] Opus engine fetching: don't ignore non-missing errors (#1555) * Opus engine fetching: don't ignore non-missing errors * typo fix --- src/client/voice/opus/OpusEngineList.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js index 447f0afdb..3b69cdaeb 100644 --- a/src/client/voice/opus/OpusEngineList.js +++ b/src/client/voice/opus/OpusEngineList.js @@ -9,7 +9,10 @@ function fetch(Encoder, engineOptions) { try { return new Encoder(engineOptions); } catch (err) { - return null; + if (err.message.includes('Cannot find module')) return null; + + // The Opus engine exists, but another error occurred. + throw err; } } From 63f5652ac2b7991a13da7090ca042aecd57c1c97 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 1 Jun 2017 03:38:34 -0500 Subject: [PATCH 0073/1359] object.entries (#1549) * Update DiscordAPIError.js * Update ClientUserSettings.js --- src/client/rest/DiscordAPIError.js | 12 ++++++------ src/structures/ClientUserSettings.js | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/client/rest/DiscordAPIError.js b/src/client/rest/DiscordAPIError.js index c5bd31b15..94f491ddc 100644 --- a/src/client/rest/DiscordAPIError.js +++ b/src/client/rest/DiscordAPIError.js @@ -24,15 +24,15 @@ class DiscordAPIError extends Error { static flattenErrors(obj, key = '') { let messages = []; - for (const k of Object.keys(obj)) { + for (const [k, v] of Object.entries(obj)) { const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k; - if (obj[k]._errors) { - messages.push(`${newKey}: ${obj[k]._errors.map(e => e.message).join(' ')}`); - } else if (obj[k].code || obj[k].message) { - messages.push(`${obj[k].code ? `${obj[k].code}: ` : ''}${obj[k].message}`.trim()); + if (v._errors) { + messages.push(`${newKey}: ${v._errors.map(e => e.message).join(' ')}`); + } else if (v.code || v.message) { + messages.push(`${v.code ? `${v.code}: ` : ''}${v.message}`.trim()); } else { - messages = messages.concat(this.flattenErrors(obj[k], newKey)); + messages = messages.concat(this.flattenErrors(v, newKey)); } } diff --git a/src/structures/ClientUserSettings.js b/src/structures/ClientUserSettings.js index 7ba29f71a..3c095df6d 100644 --- a/src/structures/ClientUserSettings.js +++ b/src/structures/ClientUserSettings.js @@ -15,8 +15,7 @@ class ClientUserSettings { * @param {Object} data Data to patch this with */ patch(data) { - for (const key of Object.keys(Constants.UserSettingsMap)) { - const value = Constants.UserSettingsMap[key]; + for (const [key, value] of Object.entries(Constants.UserSettingsMap)) { if (!data.hasOwnProperty(key)) continue; if (typeof value === 'function') { this[value.name] = value(data[key]); From 375b89a94a91bbee7db9dd3b28a2be48e4214d0b Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 1 Jun 2017 10:42:35 +0200 Subject: [PATCH 0074/1359] Force node 8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 698635a26..b85f17446 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "webpack": "^2.2.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=8.0.0" }, "browser": { "ws": false, From 37d6624ef17216daeed27a79eb9227cac92d9940 Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 1 Jun 2017 10:45:56 +0200 Subject: [PATCH 0075/1359] Fix CRLF to LF --- .gitignore | 44 +++++++------- docs/README.md | 2 +- test/voice.js | 156 ++++++++++++++++++++++++------------------------- 3 files changed, 101 insertions(+), 101 deletions(-) diff --git a/.gitignore b/.gitignore index b4bdf8b6b..55693512d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,22 @@ -# Packages -node_modules/ -yarn.lock -package-lock.json - -# Log files -logs/ -*.log - -# Authentication -test/auth.json -test/auth.js -docs/deploy/deploy_key -docs/deploy/deploy_key.pub -deploy/deploy_key -deploy/deploy_key.pub - -# Miscellaneous -.tmp/ -.vscode/ -docs/docs.json -webpack/ +# Packages +node_modules/ +yarn.lock +package-lock.json + +# Log files +logs/ +*.log + +# Authentication +test/auth.json +test/auth.js +docs/deploy/deploy_key +docs/deploy/deploy_key.pub +deploy/deploy_key +deploy/deploy_key.pub + +# Miscellaneous +.tmp/ +.vscode/ +docs/docs.json +webpack/ diff --git a/docs/README.md b/docs/README.md index d41af8eaf..b5ac7978f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1 +1 @@ -## [View the documentation here.](https://discord.js.org/#/docs) +## [View the documentation here.](https://discord.js.org/#/docs) diff --git a/test/voice.js b/test/voice.js index 396bcc490..65f4329e7 100644 --- a/test/voice.js +++ b/test/voice.js @@ -1,78 +1,78 @@ -/* eslint no-console: 0 */ -'use strict'; - -const Discord = require('../'); -const ytdl = require('ytdl-core'); - -const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' }); - -const auth = require('./auth.json'); - -client.login(auth.token).then(() => console.log('logged')).catch(console.error); - -const connections = new Map(); - -let broadcast; - -client.on('message', m => { - if (!m.guild) return; - if (m.content.startsWith('/join')) { - const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel; - if (channel && channel.type === 'voice') { - channel.join().then(conn => { - conn.player.on('error', (...e) => console.log('player', ...e)); - if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); - m.reply('ok!'); - }); - } else { - m.reply('Specify a voice channel!'); - } - } else if (m.content.startsWith('/play')) { - if (connections.has(m.guild.id)) { - const connData = connections.get(m.guild.id); - const queue = connData.queue; - const url = m.content.split(' ').slice(1).join(' ') - .replace(//g, ''); - queue.push({ url, m }); - if (queue.length > 1) { - m.reply(`OK, that's going to play after ${queue.length - 1} songs`); - return; - } - doQueue(connData); - } - } else if (m.content.startsWith('/skip')) { - if (connections.has(m.guild.id)) { - const connData = connections.get(m.guild.id); - if (connData.dispatcher) { - connData.dispatcher.end(); - } - } - } else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') { - try { - const com = eval(m.content.split(' ').slice(1).join(' ')); - m.channel.sendMessage(`\`\`\`\n${com}\`\`\``); - } catch (e) { - console.log(e); - m.channel.sendMessage(`\`\`\`\n${e}\`\`\``); - } - } -}); - -function doQueue(connData) { - const conn = connData.conn; - const queue = connData.queue; - const item = queue[0]; - if (!item) return; - const stream = ytdl(item.url, { filter: 'audioonly' }, { passes: 3 }); - const dispatcher = conn.playStream(stream); - stream.on('info', info => { - item.m.reply(`OK, playing **${info.title}**`); - }); - dispatcher.on('end', () => { - queue.shift(); - doQueue(connData); - }); - dispatcher.on('error', (...e) => console.log('dispatcher', ...e)); - connData.dispatcher = dispatcher; -} +/* eslint no-console: 0 */ +'use strict'; + +const Discord = require('../'); +const ytdl = require('ytdl-core'); + +const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' }); + +const auth = require('./auth.json'); + +client.login(auth.token).then(() => console.log('logged')).catch(console.error); + +const connections = new Map(); + +let broadcast; + +client.on('message', m => { + if (!m.guild) return; + if (m.content.startsWith('/join')) { + const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel; + if (channel && channel.type === 'voice') { + channel.join().then(conn => { + conn.player.on('error', (...e) => console.log('player', ...e)); + if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); + m.reply('ok!'); + }); + } else { + m.reply('Specify a voice channel!'); + } + } else if (m.content.startsWith('/play')) { + if (connections.has(m.guild.id)) { + const connData = connections.get(m.guild.id); + const queue = connData.queue; + const url = m.content.split(' ').slice(1).join(' ') + .replace(//g, ''); + queue.push({ url, m }); + if (queue.length > 1) { + m.reply(`OK, that's going to play after ${queue.length - 1} songs`); + return; + } + doQueue(connData); + } + } else if (m.content.startsWith('/skip')) { + if (connections.has(m.guild.id)) { + const connData = connections.get(m.guild.id); + if (connData.dispatcher) { + connData.dispatcher.end(); + } + } + } else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') { + try { + const com = eval(m.content.split(' ').slice(1).join(' ')); + m.channel.sendMessage(`\`\`\`\n${com}\`\`\``); + } catch (e) { + console.log(e); + m.channel.sendMessage(`\`\`\`\n${e}\`\`\``); + } + } +}); + +function doQueue(connData) { + const conn = connData.conn; + const queue = connData.queue; + const item = queue[0]; + if (!item) return; + const stream = ytdl(item.url, { filter: 'audioonly' }, { passes: 3 }); + const dispatcher = conn.playStream(stream); + stream.on('info', info => { + item.m.reply(`OK, playing **${info.title}**`); + }); + dispatcher.on('end', () => { + queue.shift(); + doQueue(connData); + }); + dispatcher.on('error', (...e) => console.log('dispatcher', ...e)); + connData.dispatcher = dispatcher; +} From a2520efa1a3b971126f0ef0b3008de1e4289dae4 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 1 Jun 2017 14:14:42 -0500 Subject: [PATCH 0076/1359] Use node 8 error code (#1557) --- src/client/voice/opus/OpusEngineList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js index 3b69cdaeb..152481745 100644 --- a/src/client/voice/opus/OpusEngineList.js +++ b/src/client/voice/opus/OpusEngineList.js @@ -9,7 +9,7 @@ function fetch(Encoder, engineOptions) { try { return new Encoder(engineOptions); } catch (err) { - if (err.message.includes('Cannot find module')) return null; + if (err.code === 'MODULE_NOT_FOUND') return null; // The Opus engine exists, but another error occurred. throw err; From 45cf05746dffcd140dadb008b8aad531c1f1e87d Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 2 Jun 2017 00:12:27 -0400 Subject: [PATCH 0077/1359] Revise Travis scripts, and use only Node 8 --- .travis.yml | 4 +--- deploy/deploy.sh | 14 ++++++-------- deploy/test.sh | 16 +++------------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7c558aaba..1a32d6a95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: node_js node_js: - - "6" - - "7" - "8" cache: directories: @@ -11,7 +9,7 @@ script: bash ./deploy/test.sh jobs: include: - stage: build - node_js: "6" + node_js: "8" script: bash ./deploy/deploy.sh env: global: diff --git a/deploy/deploy.sh b/deploy/deploy.sh index d12ee608b..fe9f1ab19 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -3,11 +3,6 @@ set -e -function build { - npm run docs - VERSIONED=false npm run webpack -} - # For revert branches, do nothing if [[ "$TRAVIS_BRANCH" == revert-* ]]; then echo -e "\e[36m\e[1mBuild triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing." @@ -31,13 +26,16 @@ else SOURCE_TYPE="branch" fi -# For Node != 6, do nothing -if [ "$TRAVIS_NODE_VERSION" != "6" ]; then +# 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 -build +# Run the build +npm run docs +VERSIONED=false npm run webpack + # Initialise some useful variables REPO=`git config remote.origin.url` diff --git a/deploy/test.sh b/deploy/test.sh index 9e076a4c4..52812d4eb 100644 --- a/deploy/test.sh +++ b/deploy/test.sh @@ -2,12 +2,6 @@ set -e -function tests { - npm run lint - npm run docs:test - exit 0 -} - # For revert branches, do nothing if [[ "$TRAVIS_BRANCH" == revert-* ]]; then echo -e "\e[36m\e[1mTest triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing." @@ -16,8 +10,7 @@ fi # For PRs if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "\e[36m\e[1mTest triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - only running tests." - tests + echo -e "\e[36m\e[1mTest triggered for PR #${TRAVIS_PULL_REQUEST}." fi # Figure out the source of the test @@ -27,8 +20,5 @@ else echo -e "\e[36m\e[1mTest triggered for branch \"${TRAVIS_BRANCH}\"." fi -# For Node != 6 -if [ "$TRAVIS_NODE_VERSION" != "6" ]; then - echo -e "\e[36m\e[1mTest triggered with Node v${TRAVIS_NODE_VERSION} - only running tests." - tests -fi +# Run the tests +npm test From 2a96296e95d39846d40d7ecc0c1b0980928e5238 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 2 Jun 2017 00:19:02 -0400 Subject: [PATCH 0078/1359] Identify version as development --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b85f17446..594c2f506 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord.js", - "version": "12.0.0", + "version": "12.0.0-dev", "description": "A powerful library for interacting with the Discord API", "main": "./src/index", "types": "./typings/index.d.ts", From 92e9c61eb6d1107161eb7bebb9491e611288a81e Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 2 Jun 2017 00:25:13 -0400 Subject: [PATCH 0079/1359] Betterer Travis? Maybe? --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1a32d6a95..d6acd934e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,11 @@ cache: directories: - node_modules install: npm install -script: bash ./deploy/test.sh jobs: include: + - stage: test + script: bash ./deploy/test.sh - stage: build - node_js: "8" script: bash ./deploy/deploy.sh env: global: From 2f2481c65eb4eee8879bfd3cc8c3129a0f317966 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 2 Jun 2017 00:34:18 -0400 Subject: [PATCH 0080/1359] Clean up Travis stuff MOAR --- .travis.yml | 6 +++--- {deploy => travis}/deploy-key.enc | Bin {deploy => travis}/deploy.sh | 3 +-- {deploy => travis}/test.sh | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) rename {deploy => travis}/deploy-key.enc (100%) rename {deploy => travis}/deploy.sh (95%) rename {deploy => travis}/test.sh (99%) diff --git a/.travis.yml b/.travis.yml index d6acd934e..fb2f6214c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,9 @@ install: npm install jobs: include: - stage: test - script: bash ./deploy/test.sh - - stage: build - script: bash ./deploy/deploy.sh + script: bash ./travis/test.sh + - stage: deploy + script: bash ./travis/deploy.sh env: global: - ENCRYPTION_LABEL: "af862fa96d3e" diff --git a/deploy/deploy-key.enc b/travis/deploy-key.enc similarity index 100% rename from deploy/deploy-key.enc rename to travis/deploy-key.enc diff --git a/deploy/deploy.sh b/travis/deploy.sh similarity index 95% rename from deploy/deploy.sh rename to travis/deploy.sh index fe9f1ab19..08264264e 100644 --- a/deploy/deploy.sh +++ b/travis/deploy.sh @@ -1,6 +1,5 @@ #!/bin/bash # Adapted from https://gist.github.com/domenic/ec8b0fc8ab45f39403dd. - set -e # For revert branches, do nothing @@ -47,7 +46,7 @@ ENCRYPTED_KEY_VAR="encrypted_${ENCRYPTION_LABEL}_key" ENCRYPTED_IV_VAR="encrypted_${ENCRYPTION_LABEL}_iv" ENCRYPTED_KEY=${!ENCRYPTED_KEY_VAR} ENCRYPTED_IV=${!ENCRYPTED_IV_VAR} -openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in deploy/deploy-key.enc -out deploy-key -d +openssl aes-256-cbc -K $ENCRYPTED_KEY -iv $ENCRYPTED_IV -in travis/deploy-key.enc -out deploy-key -d chmod 600 deploy-key eval `ssh-agent -s` ssh-add deploy-key diff --git a/deploy/test.sh b/travis/test.sh similarity index 99% rename from deploy/test.sh rename to travis/test.sh index 52812d4eb..bef6134e9 100644 --- a/deploy/test.sh +++ b/travis/test.sh @@ -1,5 +1,4 @@ #!/bin/bash - set -e # For revert branches, do nothing From 28dc3e6a2e95871b5dc674785ef69b2faf18fe3f Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Fri, 2 Jun 2017 00:36:34 -0400 Subject: [PATCH 0081/1359] Utilise Object.values for Permissions.ALL --- src/util/Permissions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 9b72440a2..04b1864fd 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -171,7 +171,7 @@ Permissions.FLAGS = { * Bitfield representing every permission combined * @type {number} */ -Permissions.ALL = Object.keys(Permissions.FLAGS).reduce((all, p) => all | Permissions.FLAGS[p], 0); +Permissions.ALL = Object.values(Permissions.FLAGS).reduce((all, p) => all | p, 0); /** * Bitfield representing the default permissions for users From 4e79a885b82ad0bc66d3bc62fefef561c31099fb Mon Sep 17 00:00:00 2001 From: aemino Date: Sun, 4 Jun 2017 21:52:31 -0700 Subject: [PATCH 0082/1359] Remove unused VoiceBroadcast#guaranteeOpusEngine (fixes #1556) (#1563) --- src/client/voice/VoiceBroadcast.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index b80efaf4a..c4562f17f 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -242,8 +242,6 @@ class VoiceBroadcast extends VolumeInterface { * @returns {VoiceBroadcast} */ playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) { - this.guaranteeOpusEngine(); - const options = { seek, volume, passes, input }; return this._playTranscodable(input, options); } @@ -272,10 +270,6 @@ class VoiceBroadcast extends VolumeInterface { } } - guaranteeOpusEngine() { - if (!this.opusEncoder) throw new Error('Couldn\'t find an Opus engine.'); - } - _startPlaying() { if (this.tickInterval) clearInterval(this.tickInterval); // Old code? From 577ab37a2b5adedaa31e8896a96d0a540f8de726 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 7 Jun 2017 23:47:17 +0200 Subject: [PATCH 0083/1359] make User#displayAvatarURL a method and make it and avatarURL accept an options object (#1569) * make User#displayAvatarURL a method * make avatarURL and displayAvatarURL accept an object as options --- src/structures/User.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/structures/User.js b/src/structures/User.js index 08056184c..9faae8d88 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -105,12 +105,13 @@ class User { /** * A link to the user's avatar - * @param {string} [format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, it will be `gif` - * for animated avatars or otherwise `webp` - * @param {number} [size=128] One of `128`, '256', `512`, `1024`, `2048` - * @returns {?string} avatarURL + * @param {Object} [options={}] Options for the avatar url + * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, + * it will be `gif` for animated avatars or otherwise `webp` + * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` + * @returns {?string} */ - avatarURL(format, size) { + avatarURL({ format, size } = {}) { if (!this.avatar) return null; if (typeof format === 'number') { size = format; @@ -130,11 +131,14 @@ class User { /** * A link to the user's avatar if they have one. Otherwise a link to their default avatar will be returned - * @type {string} - * @readonly + * @param {Object} [options={}] Options for the avatar url + * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, + * it will be `gif` for animated avatars or otherwise `webp` + * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` + * @returns {string} */ - get displayAvatarURL() { - return this.avatarURL() || this.defaultAvatarURL; + displayAvatarURL(options) { + return this.avatarURL(options) || this.defaultAvatarURL; } /** From 1e47cfdd5df1654ac6ed47146e09f34e3e49b0c5 Mon Sep 17 00:00:00 2001 From: FireController1847 Date: Wed, 7 Jun 2017 15:47:45 -0600 Subject: [PATCH 0084/1359] Add denied/allowed permissions to PermissionOverwrites (#1562) * Add denied/allowed permissions to PermissionOverwrites * Remove one accidental trailing space. * Change to _denied/_allowed & denied/allowed This could possible break if people use deny/allow, I assume, but that's okay. * Update PermissionOverwrites.js * Update PermissionOverwrites.js --- src/structures/PermissionOverwrites.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 3ffcaf98b..04b4ef73f 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -1,3 +1,5 @@ +const Permissions = require('../util/Permissions'); + /** * Represents a permission overwrite for a role or member in a guild channel. */ @@ -27,8 +29,20 @@ class PermissionOverwrites { */ this.type = data.type; - this.deny = data.deny; - this.allow = data.allow; + this._denied = data.deny; + this._allowed = data.allow; + + /** + * The permissions that are denied for the user or role. + * @type {Permissions} + */ + this.denied = new Permissions(this._denied); + + /** + * The permissions that are allowed for the user or role. + * @type {Permissions} + */ + this.allowed = new Permissions(this._allowed); } /** From 06f4c679d39adc1b36b65dcc9663e930ce56bb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89velyne=20Lachance?= Date: Wed, 7 Jun 2017 18:52:41 -0400 Subject: [PATCH 0085/1359] Add `count` optional argument to Collection methods (#1552) * Add `count` optional argument to Collection methods [NON-BREAKING CHANGE] An optional `count` argument is added to the following methods: - random() and randomKey() - first() and firstKey() - last() and lastKey() If `count` is used, the method returns an array instead of only the value. Performance impact non-existent for existing code. Performance for returning an array has been measured and this is the fastest I could find (array[i] = value is faster than array.push()). * Update Collection.js Fixed spacing/line length errors according to suggestions by codacy/pr * Fixed docs Added proper `@returns {*|Array}` as the methods might return either. Also added params where missing (whoops) * Further doc fixes Per Crawl's comments, fixed (i + 1) spacing as well as fixed {Integer} to {number} * random() and randomKey() fix Per Hydra's comment, random() and randomKey() now ensures unique values. I've also resolved potential issues with requesting a count higher than the collection size. A collection with 10 items will only ever return at most 10 items using the `count` property. * Can I facepalm harder Had wrong header comments ^_^ * Fixed for "values/value" and Omited Also, added "Positive" integer check. * looks like I "omitted" a change, there. * Update Collection.js * Update Collection.js * Update Collection.js --- src/util/Collection.js | 100 ++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index 620d4565d..b95af8562 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -59,59 +59,99 @@ class Collection extends Map { } /** - * Obtains the first item in this collection. - * @returns {*} + * Obtains the first value(s) in this collection. + * @param {number} [count] Number of values to obtain from the beginning + * @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length */ - first() { - return this.values().next().value; + first(count) { + if (count === undefined) return this.values().next().value; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + count = Math.min(this.size, count); + const arr = new Array(count); + const iter = this.values(); + for (let i = 0; i < count; i++) arr[i] = iter.next().value; + return arr; } /** - * Obtains the first key in this collection. - * @returns {*} + * Obtains the first key(s) in this collection. + * @param {number} [count] Number of keys to obtain from the beginning + * @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length */ - firstKey() { - return this.keys().next().value; + firstKey(count) { + if (count === undefined) return this.keys().next().value; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + count = Math.min(this.size, count); + const arr = new Array(count); + const iter = this.iter(); + for (let i = 0; i < count; i++) arr[i] = iter.next().value; + return arr; } /** - * Obtains the last item in this collection. This relies on the `array()` method, and thus the caching mechanism - * applies here as well. - * @returns {*} + * Obtains the last value(s) in this collection. This relies on {@link Collection#array}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of values to obtain from the end + * @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length */ - last() { + last(count) { const arr = this.array(); - return arr[arr.length - 1]; + if (count === undefined) return arr[arr.length - 1]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + return arr.slice(-count); } /** - * Obtains the last key in this collection. This relies on the `keyArray()` method, and thus the caching mechanism - * applies here as well. - * @returns {*} + * Obtains the last key(s) in this collection. This relies on {@link Collection#keyArray}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of keys to obtain from the end + * @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length */ - lastKey() { + lastKey(count) { const arr = this.keyArray(); - return arr[arr.length - 1]; + if (count === undefined) return arr[arr.length - 1]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + return arr.slice(-count); } /** - * Obtains a random item from this collection. This relies on the `array()` method, and thus the caching mechanism - * applies here as well. - * @returns {*} + * Obtains random value(s) from this collection. This relies on {@link Collection#array}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of values to obtain randomly + * @returns {*|Array<*>} The single value if `count` is undefined, or an array of values of `count` length */ - random() { - const arr = this.array(); - return arr[Math.floor(Math.random() * arr.length)]; + random(count) { + let arr = this.array(); + if (count === undefined) return arr[Math.floor(Math.random() * arr.length)]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + if (arr.length === 0) return []; + const rand = new Array(count); + arr = arr.slice(); + for (let i = 0; i < count; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; + return rand; } /** - * Obtains a random key from this collection. This relies on the `keyArray()` method, and thus the caching mechanism - * applies here as well. - * @returns {*} + * Obtains random key(s) from this collection. This relies on {@link Collection#keyArray}, and thus the caching + * mechanism applies here as well. + * @param {number} [count] Number of keys to obtain randomly + * @returns {*|Array<*>} The single key if `count` is undefined, or an array of keys of `count` length */ - randomKey() { - const arr = this.keyArray(); - return arr[Math.floor(Math.random() * arr.length)]; + randomKey(count) { + let arr = this.keyArray(); + if (count === undefined) return arr[Math.floor(Math.random() * arr.length)]; + if (typeof count !== 'number') throw new TypeError('The count must be a number.'); + if (!Number.isInteger(count) || count < 1) throw new RangeError('The count must be an integer greater than 0.'); + if (arr.length === 0) return []; + const rand = new Array(count); + arr = arr.slice(); + for (let i = 0; i < count; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; + return rand; } /** From 3bf8192812b28d90bfa7b2937726de29a6199c6e Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Wed, 7 Jun 2017 23:00:43 -0400 Subject: [PATCH 0086/1359] Switch to User#tag in web builds example --- docs/topics/web.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/web.md b/docs/topics/web.md index 589382e17..0adb79526 100644 --- a/docs/topics/web.md +++ b/docs/topics/web.md @@ -30,7 +30,7 @@ The usage of the API isn't any different from using it in Node.js. client.on('message', msg => { const guildTag = msg.channel.type === 'text' ? `[${msg.guild.name}]` : '[DM]'; const channelTag = msg.channel.type === 'text' ? `[#${msg.channel.name}]` : ''; - console.log(`${guildTag}${channelTag} ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`); + console.log(`${guildTag}${channelTag} ${msg.author.tag}: ${msg.content}`); }); client.login('some crazy token'); From 0898e1dd5b07d6c00c8fccfa64359901e6f9f01a Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 8 Jun 2017 14:50:26 +0200 Subject: [PATCH 0087/1359] Fix createMessageCollector example --- src/structures/interfaces/TextBasedChannel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index f76350299..d0239390b 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -292,11 +292,11 @@ class TextBasedChannel { * @returns {MessageCollector} * @example * // Create a message collector - * const collector = channel.createCollector( + * const collector = channel.createMessageCollector( * m => m.content.includes('discord'), * { time: 15000 } * ); - * collector.on('message', m => console.log(`Collected ${m.content}`)); + * collector.on('collect', m => console.log(`Collected ${m.content}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ createMessageCollector(filter, options = {}) { From 7b3a005b651690aae0f2180a8ebf392e328675e4 Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 8 Jun 2017 14:51:21 +0200 Subject: [PATCH 0088/1359] Remove createCollector from all channels --- src/structures/DMChannel.js | 2 +- src/structures/GroupDMChannel.js | 2 +- src/structures/TextChannel.js | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 0f61f773f..06701d14f 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -47,7 +47,7 @@ class DMChannel extends Channel { stopTyping() {} get typing() {} get typingCount() {} - createCollector() {} + createMessageCollector() {} awaitMessages() {} // Doesn't work on DM channels; bulkDelete() {} acknowledge() {} diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index f197b6728..9f8ed77cb 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -164,7 +164,7 @@ class GroupDMChannel extends Channel { stopTyping() {} get typing() {} get typingCount() {} - createCollector() {} + createMessageCollector() {} awaitMessages() {} // Doesn't work on Group DMs; bulkDelete() {} acknowledge() {} diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 0f0d05ba9..c45b521c1 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -96,7 +96,6 @@ class TextChannel extends GuildChannel { stopTyping() {} get typing() {} get typingCount() {} - createCollector() {} createMessageCollector() {} awaitMessages() {} bulkDelete() {} From d6041f9fb379b6652140600a3789c30ca4e9ce56 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 9 Jun 2017 16:27:49 +0200 Subject: [PATCH 0089/1359] Added Client#status and Message#type typedefs (#1571) * typedef for MessageTypes and linked Client#status to Status * should be singular * typedef for Voiceconnection#status --- src/client/Client.js | 2 +- src/client/voice/VoiceConnection.js | 2 +- src/structures/Message.js | 2 +- src/util/Constants.js | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 0fd8c18e6..c9e0e09e2 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -198,7 +198,7 @@ class Client extends EventEmitter { /** * Current status of the client's connection to Discord - * @type {?number} + * @type {?Status} * @readonly */ get status() { diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index ceab3cf14..e1297ee82 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -52,7 +52,7 @@ class VoiceConnection extends EventEmitter { /** * The current status of the voice connection - * @type {number} + * @type {VoiceStatus} */ this.status = Constants.VoiceStatus.AUTHENTICATING; diff --git a/src/structures/Message.js b/src/structures/Message.js index e87c33434..6a5fdd3dc 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -40,7 +40,7 @@ class Message { /** * The type of the message - * @type {string} + * @type {MessageType} */ this.type = Constants.MessageTypes[data.type]; diff --git a/src/util/Constants.js b/src/util/Constants.js index fdcc78d7b..64d9e5963 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -329,6 +329,18 @@ exports.WSEvents = { RELATIONSHIP_REMOVE: 'RELATIONSHIP_REMOVE', }; +/** + * The type of a message, e.g. `DEFAULT`. Here are the available types: + * - DEFAULT + * - RECIPIENT_ADD + * - RECIPIENT_REMOVE + * - CALL + * - CHANNEL_NAME_CHANGE + * - CHANNEL_ICON_CHANGE + * - PINS_ADD + * - GUILD_MEMBER_JOIN + * @typedef {string} MessageType + */ exports.MessageTypes = [ 'DEFAULT', 'RECIPIENT_ADD', From 355b1cca4a527105526ba08b825f95633fc49081 Mon Sep 17 00:00:00 2001 From: Drahcirius Date: Sat, 10 Jun 2017 18:54:17 -0400 Subject: [PATCH 0090/1359] Remove global flag from ffmpeg tutorial doc (#1582) --- docs/topics/voice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index fc9e81237..dc70eb0e9 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -4,7 +4,7 @@ Voice in discord.js can be used for many things, such as music bots, recording o In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `VoiceConnection`, where you can start streaming and receiving audio. To get started, make sure you have: -* ffmpeg - `npm install --global ffmpeg-binaries` +* ffmpeg - `npm install ffmpeg-binaries` * an opus encoder, choose one from below: * `npm install opusscript` * `npm install node-opus` From b694ab1b8098f1f6664d350248e8f9008414f5ba Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 11 Jun 2017 11:36:58 +0100 Subject: [PATCH 0091/1359] Add internal sharding options --- src/client/Client.js | 6 ++++++ src/util/Constants.js | 1 + 2 files changed, 7 insertions(+) diff --git a/src/client/Client.js b/src/client/Client.js index c9e0e09e2..107a2f0e9 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -552,6 +552,12 @@ class Client extends EventEmitter { if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { throw new TypeError('The restWsBridgeTimeout option must be a number.'); } + if (typeof options.internalSharding !== 'boolean') { + throw new TypeError('The internalSharding option must be a boolean.'); + } + if (options.internalSharding && ('shardCount' in options || 'shardId' in options)) { + throw new TypeError('You cannot specify shardCount/shardId if you are using internal sharding.'); + } if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.'); } } diff --git a/src/util/Constants.js b/src/util/Constants.js index 64d9e5963..9bee754b0 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -34,6 +34,7 @@ exports.DefaultOptions = { apiRequestMethod: 'sequential', shardId: 0, shardCount: 0, + internalSharding: false, messageCacheMaxSize: 200, messageCacheLifetime: 0, messageSweepInterval: 0, From ecb8424f527719be4427084d63f09fd0fe19766a Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 12 Jun 2017 17:26:36 +0200 Subject: [PATCH 0092/1359] fixed typo in Guild#createEmoji (#1588) --- 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 61c3d5d25..8d94b4b42 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -941,7 +941,7 @@ class Guild { * .catch(console.error); */ createEmoji(attachment, name, roles) { - if (typeof attahment === 'string' && attachment.startsWith('data:')) { + if (typeof attachment === 'string' && attachment.startsWith('data:')) { const data = { image: attachment, name }; if (roles) data.roles = roles.map(r => r.id ? r.id : r); return this.client.api.guilds(this.id).emojis.post({ data }) From 35e8601b3a213f0d4f4b3bd55e21faadae06f4b0 Mon Sep 17 00:00:00 2001 From: DeJay Date: Tue, 13 Jun 2017 15:55:54 -0400 Subject: [PATCH 0093/1359] Replacing ticks (#1589) http://i.imgur.com/7Xbaawm.png --- src/structures/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/User.js b/src/structures/User.js index 9faae8d88..0b85875a2 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -108,7 +108,7 @@ class User { * @param {Object} [options={}] Options for the avatar url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, * it will be `gif` for animated avatars or otherwise `webp` - * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` + * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` * @returns {?string} */ avatarURL({ format, size } = {}) { From 00eebd34cb76e5366285025cb4deccfa0840618e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 16 Jun 2017 12:49:13 +0200 Subject: [PATCH 0094/1359] Correctly mapping overwrites when creating a channel and renamed all relevant property names as of #1562 (#1570) * using correct properties to apply permissionOverwrites and fixed `GuildChannel#clone` * also arrays should be mapped and correct properties taking priority * changed .deny and .allow to .denied and .allowed respectively * whoops --- src/structures/Guild.js | 9 ++++++++- src/structures/GuildChannel.js | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 8d94b4b42..e9abbda85 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -843,7 +843,14 @@ class Guild { * .catch(console.error); */ createChannel(name, type, { overwrites, reason } = {}) { - if (overwrites instanceof Collection) overwrites = overwrites.array(); + if (overwrites instanceof Collection || overwrites instanceof Array) { + overwrites = overwrites.map(overwrite => ({ + allow: overwrite.allow || overwrite._allowed, + deny: overwrite.deny || overwrite._denied, + type: overwrite.type, + id: overwrite.id, + })); + } return this.client.api.guilds(this.id).channels.post({ data: { name, type, permission_overwrites: overwrites, diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 81976e987..0ea97a9f6 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -76,20 +76,20 @@ class GuildChannel extends Channel { const overwrites = this.overwritesFor(member, true, roles); if (overwrites.everyone) { - permissions &= ~overwrites.everyone.deny; - permissions |= overwrites.everyone.allow; + permissions &= ~overwrites.everyone._denied; + permissions |= overwrites.everyone._allowed; } let allow = 0; for (const overwrite of overwrites.roles) { - permissions &= ~overwrite.deny; - allow |= overwrite.allow; + permissions &= ~overwrite._denied; + allow |= overwrite._allowed; } permissions |= allow; if (overwrites.member) { - permissions &= ~overwrites.member.deny; - permissions |= overwrites.member.allow; + permissions &= ~overwrites.member._denied; + permissions |= overwrites.member._allowed; } const admin = Boolean(permissions & Permissions.FLAGS.ADMINISTRATOR); @@ -171,8 +171,8 @@ class GuildChannel extends Channel { const prevOverwrite = this.permissionOverwrites.get(userOrRole.id); if (prevOverwrite) { - payload.allow = prevOverwrite.allow; - payload.deny = prevOverwrite.deny; + payload.allow = prevOverwrite._allowed; + payload.deny = prevOverwrite._denied; } for (const perm in options) { @@ -290,7 +290,7 @@ class GuildChannel extends Channel { return this.client.api.channels(this.id).invites.post({ data: { temporary, max_age: maxAge, max_uses: maxUses, }, reason }) - .then(invite => new Invite(this.client, invite)); + .then(invite => new Invite(this.client, invite)); } /** @@ -301,7 +301,7 @@ class GuildChannel extends Channel { * @returns {Promise} */ clone(name = this.name, withPermissions = true, withTopic = true) { - return this.guild.createChannel(name, this.type, withPermissions ? this.permissionOverwrites : []) + return this.guild.createChannel(name, this.type, { overwrites: withPermissions ? this.permissionOverwrites : [] }) .then(channel => withTopic ? channel.setTopic(this.topic) : channel); } From 66cc5b2b531eb5291453f090fbf7eb6bac7f12de Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 17 Jun 2017 04:47:37 -0500 Subject: [PATCH 0095/1359] URI Encoding for reasons (#1606) --- src/client/rest/APIRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 7271e64b1..5f21f25dc 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -42,7 +42,7 @@ class APIRequest { const request = snekfetch[this.method](`${API}${this.path}`); if (this.options.auth !== false) request.set('Authorization', this.getAuth()); - if (this.options.reason) request.set('X-Audit-Log-Reason', this.options.reason); + if (this.options.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.options.reason)); if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent); if (this.options.files) { From 4f238222647aef1220e217546889bc9d664904e2 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 23 Jun 2017 21:49:56 +0200 Subject: [PATCH 0096/1359] Wait for the websocket event when creating a guild (#1524) * Wait for the websocket event when creating a guild * using TOOK_TOO_LONG error * resolve after timeout with an unavailable guild object --- src/structures/ClientUser.js | 25 ++++++++++++++++++++++--- src/structures/Guild.js | 2 +- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index beb624623..cd04ad8f3 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -298,11 +298,30 @@ class ClientUser extends User { */ createGuild(name, { region, icon = null } = {}) { if (!icon || (typeof icon === 'string' && icon.startsWith('data:'))) { - return this.client.api.guilds.post({ data: { name, region, icon } }) - .then(data => this.client.dataManager.newGuild(data)); + return new Promise((resolve, reject) => + this.client.api.guilds.post({ data: { name, region, icon } }) + .then(data => { + if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id)); + + const handleGuild = guild => { + if (guild.id === data.id) { + this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); + this.client.clearTimeout(timeout); + resolve(guild); + } + }; + this.client.on(Constants.Events.GUILD_CREATE, handleGuild); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Constants.Events.GUILD_CREATE, handleGuild); + resolve(this.client.dataManager.newGuild(data)); + }, 10000); + return undefined; + }, reject) + ); } else { return this.client.resolver.resolveBuffer(icon) - .then(data => this.createGuild(name, region, this.client.resolver.resolveBase64(data) || null)); + .then(data => this.createGuild(name, { region, icon: this.client.resolver.resolveBase64(data) || null })); } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index e9abbda85..84ab909b2 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -68,8 +68,8 @@ class Guild { */ this.id = data.id; } else { - this.available = true; this.setup(data); + if (!data.channels) this.available = false; } } From e671a010cbb5dd7ca988b78fc4b6ec96f5dd7fd9 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 25 Jun 2017 00:03:37 +0200 Subject: [PATCH 0097/1359] added Guild#setExplicitContentFilter (#1583) --- src/structures/Guild.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 84ab909b2..e3c6a18d3 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -560,6 +560,7 @@ class Guild { * @property {string} [name] The name of the guild * @property {string} [region] The region of the guild * @property {number} [verificationLevel] The verification level of the guild + * @property {number} [explicitContentFilter] The level of the explicit content filter * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild * @property {number} [afkTimeout] The AFK timeout of the guild * @property {Base64Resolvable} [icon] The icon of the guild @@ -585,16 +586,28 @@ class Guild { const _data = {}; if (data.name) _data.name = data.name; if (data.region) _data.region = data.region; - if (data.verificationLevel) _data.verification_level = Number(data.verificationLevel); + if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel); if (data.afkChannel) _data.afk_channel_id = this.client.resolver.resolveChannel(data.afkChannel).id; if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); if (data.icon) _data.icon = this.client.resolver.resolveBase64(data.icon); if (data.owner) _data.owner_id = this.client.resolver.resolveUser(data.owner).id; if (data.splash) _data.splash = this.client.resolver.resolveBase64(data.splash); + if (typeof data.explicitContentFilter !== 'undefined') { + _data.explicit_content_filter = Number(data.explicitContentFilter); + } return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } + /** + * Edit the level of the explicit content filter. + * @param {number} explicitContentFilter The new level of the explicit content filter + * @returns {Promise} + */ + setExplicitContentFilter(explicitContentFilter) { + return this.edit({ explicitContentFilter }); + } + /** * Edit the name of the guild. * @param {string} name The new name of the guild From 1fadd0f8597491e45b04e3c1b31a17c0282e125f Mon Sep 17 00:00:00 2001 From: Mstrodl Date: Sat, 24 Jun 2017 18:12:22 -0400 Subject: [PATCH 0098/1359] Update avatar example for v12.0 (#1610) User.displayAvatarURL was changed from a property to a method so I changed the example accordingly --- docs/examples/avatars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/avatars.js b/docs/examples/avatars.js index cf59a6c94..58bc90f8f 100644 --- a/docs/examples/avatars.js +++ b/docs/examples/avatars.js @@ -22,7 +22,7 @@ client.on('message', message => { // If the message is "what is my avatar" if (message.content === 'what is my avatar') { // Send the user's avatar URL - message.reply(message.author.displayAvatarURL); + message.reply(message.author.displayAvatarURL()); } }); From e506995e4fe19a326b2c64c430c1c2b09f13dd39 Mon Sep 17 00:00:00 2001 From: "Morgan (Fallen)" Date: Sat, 24 Jun 2017 23:20:29 +0100 Subject: [PATCH 0099/1359] Update voice and shard examples (#1608) * Update voice.js message.channel.sendMessage(...) to message.channel.send(...) * Update shard.js message.channel.sendMessage(...) to message.channel.send(...) * Update voice.js * Update shard.js * Update voice.js --- test/shard.js | 4 ++-- test/voice.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/shard.js b/test/shard.js index 6021a18c9..5f5f17c49 100644 --- a/test/shard.js +++ b/test/shard.js @@ -10,9 +10,9 @@ client.on('message', msg => { if (msg.content.startsWith('?eval') && msg.author.id === '66564597481480192') { try { const com = eval(msg.content.split(' ').slice(1).join(' ')); - msg.channel.sendMessage('```\n' + com + '```'); + msg.channel.send(com, { code: true }); } catch (e) { - msg.channel.sendMessage('```\n' + e + '```'); + msg.channel.send(e, { code: true }); } } }); diff --git a/test/voice.js b/test/voice.js index 65f4329e7..0b36636a3 100644 --- a/test/voice.js +++ b/test/voice.js @@ -51,10 +51,10 @@ client.on('message', m => { } else if (m.content.startsWith('#eval') && m.author.id === '66564597481480192') { try { const com = eval(m.content.split(' ').slice(1).join(' ')); - m.channel.sendMessage(`\`\`\`\n${com}\`\`\``); + m.channel.send(com, { code: true }); } catch (e) { console.log(e); - m.channel.sendMessage(`\`\`\`\n${e}\`\`\``); + m.channel.send(e, { code: true }); } } }); From fd79539ec346cb2e8de00f619bf17da14603d93a Mon Sep 17 00:00:00 2001 From: Mythic Date: Sun, 25 Jun 2017 01:21:21 +0300 Subject: [PATCH 0100/1359] Improve Message's ID attribute documentation (#1450) Remove the implication that a Message object's ID is unique only to the channel it was sent on Message ID's are snowflakes, and as stated in Discord's API documentation, globally unique throughout Discord --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 6a5fdd3dc..84bafec68 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -33,7 +33,7 @@ class Message { setup(data) { // eslint-disable-line complexity /** - * The ID of the message (unique in the channel it was sent) + * The ID of the message * @type {Snowflake} */ this.id = data.id; From 7f8cc9c297ff2cb6954ffa5faf4e49fa42215602 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 24 Jun 2017 17:23:45 -0500 Subject: [PATCH 0101/1359] sanity changes to search (#1593) * Create Search.js * Create Guild.js * Create TextBasedChannel.js * Create Search.js * Create Search.js * Create Guild.js * Create TextBasedChannel.js * Create Search.js --- src/structures/Guild.js | 6 +++--- src/structures/interfaces/TextBasedChannel.js | 6 +++--- src/structures/shared/Search.js | 21 +++++++++++++------ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index e3c6a18d3..3b6f0d9ef 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -538,7 +538,7 @@ class Guild { * Performs a search within the entire guild. * This is only available when using a user account. * @param {MessageSearchOptions} [options={}] Options to pass to the search - * @returns {Promise>} + * @returns {Promise} * An array containing arrays of messages. Each inner array is a search context cluster. * The message which has triggered the result will have the `hit` property set to `true`. * @example @@ -546,8 +546,8 @@ class Guild { * content: 'discord.js', * before: '2016-11-17' * }).then(res => { - * const hit = res.messages[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); + * const hit = res.results[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.total}`); * }).catch(console.error); */ search(options = {}) { diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index d0239390b..5d4957ded 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -203,7 +203,7 @@ class TextBasedChannel { * Performs a search within the channel. * This is only available when using a user account. * @param {MessageSearchOptions} [options={}] Options to pass to the search - * @returns {Promise>} + * @returns {Promise} * An array containing arrays of messages. Each inner array is a search context cluster * The message which has triggered the result will have the `hit` property set to `true` * @example @@ -211,8 +211,8 @@ class TextBasedChannel { * content: 'discord.js', * before: '2016-11-17' * }).then(res => { - * const hit = res.messages[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**, total results: ${res.totalResults}`); + * const hit = res.results[0].find(m => m.hit).content; + * console.log(`I found: **${hit}**, total results: ${res.total}`); * }).catch(console.error); */ search(options = {}) { diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index ce2281d7a..63606f5c9 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -11,7 +11,7 @@ const long = require('long'); * @property {UserResolvable} [author] Author to limit search * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) * @property {string} [sortBy='recent'] `recent` or `relevant` - * @property {string} [sortOrder='desc'] `asc` or `desc` + * @property {string} [sortOrder='descending'] `ascending` or `descending` * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) * @property {number} [limit=25] Maximum number of results to get (1 to 25) * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) @@ -19,7 +19,7 @@ const long = require('long'); * @property {boolean} [mentionsEveryone] If everyone is mentioned * @property {string} [linkHostname] Filter links by hostname * @property {string} [embedProvider] The name of an embed provider - * @property {string} [embedType] one of `image`, `video`, `url`, `rich` + * @property {string} [embedType] one of `image`, `video`, `url`, `rich`, or add `-` to negate (e.g. `-image`) * @property {string} [attachmentFilename] The name of an attachment * @property {string} [attachmentExtension] The extension of an attachment * @property {Date} [before] Date to find messages before @@ -28,6 +28,12 @@ const long = require('long'); * @property {boolean} [nsfw=false] Include results from NSFW channels */ +/** + * @typedef {Object} MessageSearchResult + * @type {number} total Total result count + * @type {Array} results Array of message results + */ + module.exports = function search(target, options) { if (typeof options === 'string') options = { content: options }; if (options.before) { @@ -42,11 +48,14 @@ module.exports = function search(target, options) { 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 + 86400000).shiftLeft(22).toString(); + options.maxID = long.fromNumber(t + 864e5).shiftLeft(22).toString(); } if (options.channel) options.channel = target.client.resolver.resolveChannelID(options.channel); if (options.author) options.author = target.client.resolver.resolveUserID(options.author); if (options.mentions) options.mentions = target.client.resolver.resolveUserID(options.options.mentions); + if (options.sortOrder) { + options.sortOrder = { ascending: 'asc', descending: 'desc' }[options.sortOrder] || options.sortOrder; + } options = { content: options.content, max_id: options.maxID, @@ -81,12 +90,12 @@ module.exports = function search(target, options) { let endpoint = target.client.api[target instanceof Channel ? 'channels' : 'guilds'](target.id).messages().search; return endpoint.get({ query: options }).then(body => { - const messages = body.messages.map(x => + const results = body.messages.map(x => x.map(m => new Message(target.client.channels.get(m.channel_id), m, target.client)) ); return { - totalResults: body.total_results, - messages, + total: body.total_results, + results, }; }); }; From 201ecd25a2e017c0be74ed2c68abc2c323813a87 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 25 Jun 2017 00:26:09 +0200 Subject: [PATCH 0102/1359] renamed OAuth2Application#reset to resetSecret and added resetToken (#1541) --- src/client/Client.js | 2 +- src/client/rest/APIRouter.js | 2 +- src/structures/OAuth2Application.js | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 107a2f0e9..553b9c584 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -408,7 +408,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchApplication(id = '@me') { - return this.rest.api.oauth2.applications(id).get() + return this.api.oauth2.applications(id).get() .then(app => new OAuth2Application(this, app)); } diff --git a/src/client/rest/APIRouter.js b/src/client/rest/APIRouter.js index 20133822f..be7a6ecbe 100644 --- a/src/client/rest/APIRouter.js +++ b/src/client/rest/APIRouter.js @@ -7,7 +7,7 @@ const paramable = [ 'bans', 'emojis', 'pins', 'permissions', 'reactions', 'webhooks', 'messages', 'notes', 'roles', 'applications', - 'invites', + 'invites', 'bot', ]; const reflectors = ['toString', 'valueOf', 'inspect', Symbol.toPrimitive, util.inspect.custom]; diff --git a/src/structures/OAuth2Application.js b/src/structures/OAuth2Application.js index 52eb89a12..c35dcc78c 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/OAuth2Application.js @@ -133,14 +133,25 @@ class OAuth2Application { } /** - * Reset the app's secret and bot token. + * Reset the app's secret. + * This is only available when using a user account. * @returns {OAuth2Application} */ - reset() { - return this.rest.api.oauth2.applications(this.id).reset.post() + resetSecret() { + return this.client.api.oauth2.applications(this.id).reset.post() .then(app => new OAuth2Application(this.client, app)); } + /** + * Reset the app's bot token. + * This is only available when using a user account. + * @returns {OAuth2Application} + */ + resetToken() { + return this.client.api.oauth2.applications(this.id).bot().reset.post() + .then(app => new OAuth2Application(this.client, Object.assign({}, this, { bot: app }))); + } + /** * When concatenated with a string, this automatically concatenates the app name rather than the app object. * @returns {string} From 4ae4c975892dc717be423b5ca6a6ca0ac877194e Mon Sep 17 00:00:00 2001 From: aemino Date: Sat, 24 Jun 2017 15:28:49 -0700 Subject: [PATCH 0103/1359] Fix VoiceConnection#authenticateFailed race condition (#1601) --- src/client/voice/VoiceConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index e1297ee82..ff218623f 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -245,7 +245,6 @@ class VoiceConnection extends EventEmitter { */ authenticateFailed(reason) { clearTimeout(this.connectTimeout); - this.status = Constants.VoiceStatus.DISCONNECTED; if (this.status === Constants.VoiceStatus.AUTHENTICATING) { /** * Emitted when we fail to initiate a voice connection. @@ -256,6 +255,7 @@ class VoiceConnection extends EventEmitter { } else { this.emit('error', new Error(reason)); } + this.status = Constants.VoiceStatus.DISCONNECTED; } /** From cada8763a94c6698dbe362421b478324c78482fc Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 24 Jun 2017 17:32:48 -0500 Subject: [PATCH 0104/1359] clean up cdn resources (#1597) * Create Constants.js * Update Constants.js --- src/util/Constants.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 9bee754b0..98b359b4c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -107,6 +107,11 @@ const AllowedImageSizes = [ 2048, ]; +function checkImage({ size, format }) { + if (format && !AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); + if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); +} + exports.Endpoints = { CDN(root) { return { @@ -115,30 +120,27 @@ exports.Endpoints = { DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, Avatar: (userID, hash, format = 'default', size) => { if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; - if (!AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); - if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); + checkImage({ size, format }); return `${root}/avatars/${userID}/${hash}.${format}${size ? `?size=${size}` : ''}`; }, - Icon: (guildID, hash, format = 'default', size) => { - if (format === 'default') format = 'webp'; - if (!AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); - if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); + Icon: (guildID, hash, format = 'webp', size) => { + checkImage({ size, format }); return `${root}/icons/${guildID}/${hash}.${format}${size ? `?size=${size}` : ''}`; }, - AppIcon: (clientID, hash, format = 'default', size) => { - if (format === 'default') format = 'webp'; - if (!AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); - if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); + AppIcon: (clientID, hash, format = 'webp', size) => { + checkImage({ size, format }); return `${root}/app-icons/${clientID}/${hash}.${format}${size ? `?size=${size}` : ''}`; }, - Splash: (guildID, hash) => `${root}/splashes/${guildID}/${hash}.jpg`, + Splash: (guildID, hash, format = 'webp', size) => { + checkImage({ size, format }); + return `${root}/splashes/${guildID}/${hash}.${format}${size ? `?size=${size}` : ''}`; + }, }; }, invite: code => `https://discord.gg/${code}`, botGateway: '/gateway/bot', }; - /** * The current status of the client. Here are the available statuses: * - READY From 602fe06f88bbb98bfc070419a80018c431a1ff21 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 24 Jun 2017 17:36:48 -0500 Subject: [PATCH 0105/1359] update docs for discord api error (#1575) * aaaaa * Update DiscordAPIError.js --- src/client/rest/DiscordAPIError.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/rest/DiscordAPIError.js b/src/client/rest/DiscordAPIError.js index 94f491ddc..ae2b1262e 100644 --- a/src/client/rest/DiscordAPIError.js +++ b/src/client/rest/DiscordAPIError.js @@ -1,5 +1,6 @@ /** * Represents an error from the Discord API. + * @extends Error */ class DiscordAPIError extends Error { constructor(error) { @@ -20,6 +21,7 @@ class DiscordAPIError extends Error { * @param {Object} obj Discord errors object * @param {string} [key] Used internally to determine key names of nested fields * @returns {string[]} + * @private */ static flattenErrors(obj, key = '') { let messages = []; From 63e54982f4f11d3f047858f4ebd2acccac4c81fe Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 25 Jun 2017 12:48:05 -0500 Subject: [PATCH 0106/1359] Errors Standardization (#1246) * errors and stuff * more errors * all the errors * fix build --- src/client/Client.js | 36 ++++---- src/client/ClientDataResolver.js | 7 +- src/client/ClientManager.js | 9 +- src/client/rest/APIRequest.js | 4 +- src/client/rest/RESTManager.js | 4 +- src/client/voice/ClientVoiceManager.js | 7 +- src/client/voice/VoiceConnection.js | 5 +- src/client/voice/VoiceUDPClient.js | 5 +- src/client/voice/VoiceWebSocket.js | 5 +- src/client/voice/opus/OpusEngineList.js | 4 +- src/client/voice/receiver/VoiceReceiver.js | 9 +- src/errors/DJSError.js | 65 ++++++++++++++ src/errors/Messages.js | 84 +++++++++++++++++++ src/errors/index.js | 2 + src/sharding/Shard.js | 3 +- src/sharding/ShardClientUtil.js | 3 +- src/sharding/ShardingManager.js | 27 +++--- src/structures/ClientUser.js | 3 +- src/structures/Guild.js | 6 +- src/structures/GuildMember.js | 3 +- src/structures/Message.js | 3 +- src/structures/MessageEmbed.js | 17 ++-- src/structures/interfaces/TextBasedChannel.js | 7 +- src/structures/shared/Search.js | 5 +- src/structures/shared/SendMessage.js | 3 +- src/util/Constants.js | 19 +---- src/util/Permissions.js | 4 +- src/util/Util.js | 11 ++- 28 files changed, 258 insertions(+), 102 deletions(-) create mode 100644 src/errors/DJSError.js create mode 100644 src/errors/Messages.js create mode 100644 src/errors/index.js diff --git a/src/client/Client.js b/src/client/Client.js index 553b9c584..203863451 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -19,6 +19,7 @@ const Invite = require('../structures/Invite'); const OAuth2Application = require('../structures/OAuth2Application'); const ShardClientUtil = require('../sharding/ShardClientUtil'); const VoiceBroadcast = require('./voice/VoiceBroadcast'); +const { Error, TypeError, RangeError } = require('../errors'); /** * The main hub for interacting with the Discord API, and the starting point for any bot. @@ -287,7 +288,7 @@ class Client extends EventEmitter { */ login(token) { return new Promise((resolve, reject) => { - if (typeof token !== 'string') throw new Error(Constants.Errors.INVALID_TOKEN); + if (typeof token !== 'string') throw new Error('TOKEN_INVALID'); token = token.replace(/^Bot\s*/i, ''); this.manager.connectToWebSocket(token, resolve, reject); }); @@ -375,7 +376,9 @@ class Client extends EventEmitter { * or -1 if the message cache lifetime is unlimited */ sweepMessages(lifetime = this.options.messageCacheLifetime) { - if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.'); + if (typeof lifetime !== 'number' || isNaN(lifetime)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'Lifetime', 'a number'); + } if (lifetime <= 0) { this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited'); return -1; @@ -524,41 +527,40 @@ class Client extends EventEmitter { */ _validateOptions(options = this.options) { if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) { - throw new TypeError('The shardCount option must be a number.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number'); } if (typeof options.shardId !== 'number' || isNaN(options.shardId)) { - throw new TypeError('The shardId option must be a number.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'shardId', 'a number'); } - if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.'); - if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.'); + if (options.shardCount < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 0'); + if (options.shardId < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'at least 0'); if (options.shardId !== 0 && options.shardId >= options.shardCount) { - throw new RangeError('The shardId option must be less than shardCount.'); + throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'less than shardCount'); } if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { - throw new TypeError('The messageCacheMaxSize option must be a number.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); } if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) { - throw new TypeError('The messageCacheLifetime option must be a number.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'The messageCacheLifetime', 'a number'); } if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) { - throw new TypeError('The messageSweepInterval option must be a number.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'messageSweepInterval', 'a number'); } if (typeof options.fetchAllMembers !== 'boolean') { - throw new TypeError('The fetchAllMembers option must be a boolean.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); } if (typeof options.disableEveryone !== 'boolean') { - throw new TypeError('The disableEveryone option must be a boolean.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); } if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { - throw new TypeError('The restWsBridgeTimeout option must be a number.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); } if (typeof options.internalSharding !== 'boolean') { - throw new TypeError('The internalSharding option must be a boolean.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'internalSharding', 'a boolean'); } - if (options.internalSharding && ('shardCount' in options || 'shardId' in options)) { - throw new TypeError('You cannot specify shardCount/shardId if you are using internal sharding.'); + if (!(options.disabledEvents instanceof Array)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array'); } - if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.'); } } diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index 1c268b73d..4de4c7dee 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -10,6 +10,7 @@ const Channel = require('../structures/Channel'); const GuildMember = require('../structures/GuildMember'); const Emoji = require('../structures/Emoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); +const { Error, TypeError } = require('../errors'); /** * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g. @@ -194,14 +195,14 @@ class ClientDataResolver { snekfetch.get(resource) .end((err, res) => { if (err) return reject(err); - if (!(res.body instanceof Buffer)) return reject(new TypeError('The response body isn\'t a Buffer.')); + if (!(res.body instanceof Buffer)) return reject(new TypeError('REQ_BODY_TYPE')); return resolve(res.body); }); } else { const file = path.resolve(resource); fs.stat(file, (err, stats) => { if (err) return reject(err); - if (!stats || !stats.isFile()) return reject(new Error(`The file could not be found: ${file}`)); + if (!stats || !stats.isFile()) return reject(new Error('FILE_NOT_FOUND', file)); fs.readFile(file, (err2, data) => { if (err2) reject(err2); else resolve(data); }); @@ -211,7 +212,7 @@ class ClientDataResolver { }); } - return Promise.reject(new TypeError('The resource must be a string or Buffer.')); + return Promise.reject(new TypeError('REQ_RESOURCE_TYPE')); } /** diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 4239e2e4a..4b7f0102b 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -1,5 +1,6 @@ const Constants = require('../util/Constants'); const WebSocketConnection = require('./websocket/WebSocketConnection'); +const { Error } = require('../errors'); /** * Manages the state and background tasks of the client. @@ -37,16 +38,16 @@ class ClientManager { connectToWebSocket(token, resolve, reject) { this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`); this.client.token = token; - const timeout = this.client.setTimeout(() => reject(new Error(Constants.Errors.TOOK_TOO_LONG)), 1000 * 300); + const timeout = this.client.setTimeout(() => reject(new Error('INVALID_TOKEN')), 1000 * 300); this.client.api.gateway.get().then(res => { const protocolVersion = Constants.DefaultOptions.ws.version; const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`; this.client.emit(Constants.Events.DEBUG, `Using gateway ${gateway}`); this.client.ws.connect(gateway); this.client.ws.connection.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)); + if (event.code === 4004) reject(new Error('TOKEN_INVALID')); + if (event.code === 4010) reject(new Error('SHARDING_INVALID')); + if (event.code === 4011) reject(new Error('SHARDING_REQUIRED')); }); this.client.once(Constants.Events.READY, () => { resolve(token); diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 5f21f25dc..a96249a7a 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -1,6 +1,6 @@ const querystring = require('querystring'); const snekfetch = require('snekfetch'); -const Constants = require('../../util/Constants'); +const { Error } = require('../../errors'); class APIRequest { constructor(rest, method, path, options) { @@ -28,7 +28,7 @@ class APIRequest { } else if (this.client.token) { return this.client.token; } - throw new Error(Constants.Errors.NO_TOKEN); + throw new Error('TOKEN_MISSING'); } gen() { diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 5a7e9f0ed..ca060b3b9 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -3,7 +3,7 @@ const SequentialRequestHandler = require('./RequestHandlers/Sequential'); const BurstRequestHandler = require('./RequestHandlers/Burst'); const APIRequest = require('./APIRequest'); const mountApi = require('./APIRouter'); -const Constants = require('../../util/Constants'); +const { Error } = require('../../errors'); class RESTManager { constructor(client) { @@ -39,7 +39,7 @@ class RESTManager { case 'burst': return BurstRequestHandler; default: - throw new Error(Constants.Errors.INVALID_RATE_LIMIT_METHOD); + throw new Error('RATELIMIT_INVALID_METHOD'); } } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index a6a95eee9..400a752cc 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -1,5 +1,6 @@ const Collection = require('../../util/Collection'); const VoiceConnection = require('./VoiceConnection'); +const { Error } = require('../../errors'); /** * Manages all the voice stuff for the client. @@ -44,11 +45,7 @@ class ClientVoiceManager { joinChannel(channel) { return new Promise((resolve, reject) => { if (!channel.joinable) { - if (channel.full) { - throw new Error('You do not have permission to join this voice channel; it is full.'); - } else { - throw new Error('You do not have permission to join this voice channel.'); - } + throw new Error('VOICE_JOIN_CHANNEL', channel.full); } let connection = this.connections.get(channel.guild.id); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index ff218623f..0c4af468c 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -6,6 +6,7 @@ const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/VoiceReceiver'); const EventEmitter = require('events').EventEmitter; const Prism = require('prism-media'); +const { Error } = require('../../errors'); /** * Represents a connection to a guild's voice server. @@ -341,8 +342,8 @@ class VoiceConnection extends EventEmitter { */ connect() { if (this.status !== Constants.VoiceStatus.RECONNECTING) { - if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.'); - if (this.sockets.udp) throw new Error('There is already an existing UDP connection.'); + if (this.sockets.ws) throw new Error('WS_CONNECTION_EXISTS'); + if (this.sockets.udp) throw new Error('UDP_CONNECTION_EXISTS'); } if (this.sockets.ws) this.sockets.ws.shutdown(); diff --git a/src/client/voice/VoiceUDPClient.js b/src/client/voice/VoiceUDPClient.js index 0e148b754..6e12d5e7b 100644 --- a/src/client/voice/VoiceUDPClient.js +++ b/src/client/voice/VoiceUDPClient.js @@ -2,6 +2,7 @@ const udp = require('dgram'); const dns = require('dns'); const Constants = require('../../util/Constants'); const EventEmitter = require('events').EventEmitter; +const { Error } = require('../../errors'); /** * Represents a UDP client for a Voice Connection. @@ -89,8 +90,8 @@ class VoiceConnectionUDPClient extends EventEmitter { */ send(packet) { return new Promise((resolve, reject) => { - if (!this.socket) throw new Error('Tried to send a UDP packet, but there is no socket available.'); - if (!this.discordAddress || !this.discordPort) throw new Error('Malformed UDP address or port.'); + if (!this.socket) throw new Error('UDP_SEND_FAIL'); + if (!this.discordAddress || !this.discordPort) throw new Error('UDP_ADDRESS_MALFORMED'); this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { if (error) reject(error); else resolve(packet); }); diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index 9179232fe..025c23ecc 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -1,6 +1,7 @@ const Constants = require('../../util/Constants'); const SecretKey = require('./util/SecretKey'); const EventEmitter = require('events').EventEmitter; +const { Error } = require('../../errors'); let WebSocket; try { @@ -88,9 +89,7 @@ class VoiceWebSocket extends EventEmitter { */ send(data) { return new Promise((resolve, reject) => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - throw new Error(`Voice websocket not open to send ${data}.`); - } + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data); this.ws.send(data, null, error => { if (error) reject(error); else resolve(data); }); diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js index 152481745..9ff9ad7bf 100644 --- a/src/client/voice/opus/OpusEngineList.js +++ b/src/client/voice/opus/OpusEngineList.js @@ -1,3 +1,5 @@ +const { Error } = require('../../../errors'); + const list = [ require('./NodeOpusEngine'), require('./OpusScriptEngine'), @@ -30,5 +32,5 @@ exports.fetch = engineOptions => { exports.guaranteeOpusEngine = () => { if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch()); - if (!opusEngineFound) throw new Error('Couldn\'t find an Opus engine.'); + if (!opusEngineFound) throw new Error('OPUS_ENGINE_MISSING'); }; diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index 7b1b6c8e0..e57a39d9c 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter; const secretbox = require('../util/Secretbox'); const Readable = require('./VoiceReadable'); const OpusEncoders = require('../opus/OpusEngineList'); +const { Error } = require('../../../errors'); const nonce = Buffer.alloc(24); nonce.fill(0); @@ -122,8 +123,8 @@ class VoiceReceiver extends EventEmitter { */ createOpusStream(user) { user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); - if (!user) throw new Error('Couldn\'t resolve the user to create Opus stream.'); - if (this.opusStreams.get(user.id)) throw new Error('There is already an existing stream for that user.'); + if (!user) throw new Error('VOICE_USER_MISSING'); + if (this.opusStreams.get(user.id)) throw new Error('VOICE_STREAM_EXISTS'); const stream = new Readable(); this.opusStreams.set(user.id, stream); return stream; @@ -137,8 +138,8 @@ class VoiceReceiver extends EventEmitter { */ createPCMStream(user) { user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); - if (!user) throw new Error('Couldn\'t resolve the user to create PCM stream.'); - if (this.pcmStreams.get(user.id)) throw new Error('There is already an existing stream for that user.'); + if (!user) throw new Error('VOICE_USER_MISSING'); + if (this.pcmStreams.get(user.id)) throw new Error('VOICE_STREAM_EXISTS'); const stream = new Readable(); this.pcmStreams.set(user.id, stream); return stream; diff --git a/src/errors/DJSError.js b/src/errors/DJSError.js new file mode 100644 index 000000000..b922b711d --- /dev/null +++ b/src/errors/DJSError.js @@ -0,0 +1,65 @@ +// Heavily inspired by node's `internal/errors` module + +const kCode = Symbol('code'); +const messages = new Map(); +const assert = require('assert'); +const util = require('util'); + +/** + * Extend an error of some sort into a DiscordjsError + * @param {Error} Base Base error to extend + * @returns {DiscordjsError} + */ +function makeDiscordjsError(Base) { + return class DiscordjsError extends Base { + constructor(key, ...args) { + super(message(key, args)); + this[kCode] = key; + if (Error.captureStackTrace) Error.captureStackTrace(this, DiscordjsError); + } + + get name() { + return `${super.name} [${this[kCode]}]`; + } + + get code() { + return this[kCode]; + } + }; +} + +/** + * Format the message for an error + * @param {string} key Error key + * @param {Array<*>} args Arguments to pass for util format or as function args + * @returns {string} Formatted string + */ +function message(key, args) { + assert.strictEqual(typeof key, 'string'); + const msg = messages.get(key); + assert(msg, `An invalid error message key was used: ${key}.`); + let fmt = util.format; + if (typeof msg === 'function') { + fmt = msg; + } else { + if (args === undefined || args.length === 0) return msg; + args.unshift(msg); + } + return String(fmt(...args)); +} + +/** + * Register an error code and message + * @param {string} sym Unique name for the error + * @param {*} val Value of the error + */ +function register(sym, val) { + messages.set(sym, typeof val === 'function' ? val : String(val)); +} + +module.exports = { + register, + Error: makeDiscordjsError(Error), + TypeError: makeDiscordjsError(TypeError), + RangeError: makeDiscordjsError(RangeError), +}; diff --git a/src/errors/Messages.js b/src/errors/Messages.js new file mode 100644 index 000000000..ffaaec720 --- /dev/null +++ b/src/errors/Messages.js @@ -0,0 +1,84 @@ +const { register } = require('./DJSError'); + +const Messages = { + CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`, + + TOKEN_INVALID: 'An invalid token was provided.', + TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', + + FEATURE_BOT_ONLY: 'Only bot accounts are able to make use of this feature.', + FEATURE_USER_ONLY: 'Only user accounts are able to make use of this feature.', + + WS_BAD_MESSAGE: 'A bad message was received from the websocket; either bad compression, or not JSON.', + WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', + WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, + + PERMISSIONS_INVALID: 'Invalid permission string or number.', + PERMISSIONS_INVALID_FLAG: 'Invalid bitfield flag string or number', + + RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.', + + SHARDING_INVALID: 'Invalid shard settings were provided.', + SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.', + SHARDING_CHILD_CONNECTION: 'Failed to send message to shard\'s process.', + SHARDING_PARENT_CONNECTION: 'Failed to send message to master process.', + SHARDING_NO_SHARDS: 'No shards have been spawned', + SHARDING_IN_PROCESS: 'Shards are still being spawned', + SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards`, + + COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', + COLOR_CONVERT: 'Unable to convert color to a number.', + + EMBED_FIELD_COUNT: 'MessageEmbeds may not exceed 25 fields.', + EMBED_FIELD_NAME: 'MessageEmbed field names may not exceed 256 characters or be empty.', + EMBED_FIELD_VALUE: 'MessageEmbed field values may not exceed 1024 characters or be empty.', + EMBED_FILE_LIMIT: 'You may not upload more than one file at once.', + EMBED_DESCRIPTION: 'MessageEmbed descriptions may not exceed 2048 characters.', + EMBED_FOOTER_TEXT: 'MessageEmbed footer text may not exceed 2048 characters.', + EMBED_TITLE: 'MessageEmbed titles may not exceed 256 characters.', + + FILE_NOT_FOUND: file => `File could not be found: ${file}`, + + USER_STATUS: 'User status must be a string', + SHARD_MESSAGE_FAILED: 'Failed to send message to master process.', + + VOICE_INVALID_HEARTBEAT: 'Tried to set voice heartbeat but no valid interval was specified.', + VOICE_USER_MISSING: 'Couldn\'t resolve the user to create stream.', + VOICE_STREAM_EXISTS: 'There is already an existing stream for that user.', + VOICE_JOIN_CHANNEL: (full = false) => + `You do not have permission to join this voice channel${full ? '; it is full.' : '.'}`, + + OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', + + UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', + UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.', + UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.', + + REQ_BODY_TYPE: 'The response body isn\'t a Buffer.', + REQ_RESOURCE_TYPE: 'The resource must be a string or Buffer.', + + IMAGE_FORMAT: format => `Invalid image format: ${format}`, + IMAGE_SIZE: size => `Invalid image size: ${size}`, + + MESSAGE_MISSING: 'Message not found', + MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.', + MESSAGE_NONCE_TYPE: 'Message nonce must fit in an unsigned 64-bit integer.', + + TYPING_COUNT: 'Count must be at least 1', + + SPLIT_MAX_LEN: 'Message exceeds the max length and contains no split characters.', + + BAN_RESOLVE_ID: 'Couldn\'t resolve the user ID to unban.', + + PRUNE_DAYS_TYPE: 'Days must be a number', + + SEARCH_CHANNEL_TYPE: 'Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.', + + MESSAGE_SPLIT_MISSING: 'Message exceeds the max length and contains no split characters.', + + GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', + + EMOJI_TYPE: 'Emoji must be a string or Emoji/ReactionEmoji', +}; + +for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/errors/index.js b/src/errors/index.js new file mode 100644 index 000000000..39b7582df --- /dev/null +++ b/src/errors/index.js @@ -0,0 +1,2 @@ +module.exports = require('./DJSError'); +module.exports.Messages = require('./Messages'); diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index a73447b85..f0cc51098 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -1,6 +1,7 @@ const childProcess = require('child_process'); const path = require('path'); const Util = require('../util/Util'); +const { Error } = require('../errors'); /** * Represents a Shard spawned by the ShardingManager. @@ -60,7 +61,7 @@ class Shard { const sent = this.process.send(message, err => { if (err) reject(err); else resolve(this); }); - if (!sent) throw new Error('Failed to send message to shard\'s process.'); + if (!sent) throw new Error('SHARDING_CHILD_CONNECTION'); }); } diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index b41deb5d1..549353df2 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -1,4 +1,5 @@ const Util = require('../util/Util'); +const { Error } = require('../errors'); /** * Helper class for sharded clients spawned as a child process, such as from a ShardingManager. @@ -40,7 +41,7 @@ class ShardClientUtil { const sent = process.send(message, err => { if (err) reject(err); else resolve(); }); - if (!sent) throw new Error('Failed to send message to master process.'); + if (!sent) throw new Error('SHARDING_PARENT_CONNECTION'); }); } diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index e105f3e3f..bfae42f22 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -4,6 +4,7 @@ const EventEmitter = require('events').EventEmitter; const Shard = require('./Shard'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); +const { Error, TypeError, RangeError } = require('../errors'); /** * This is a utility class that can be used to help you spawn shards of your client. Each shard is completely separate @@ -34,10 +35,10 @@ class ShardingManager extends EventEmitter { * @type {string} */ this.file = file; - if (!file) throw new Error('File must be specified.'); + if (!file) throw new Error('CLIENT_INVALID_OPTION', 'File', 'specified.'); if (!path.isAbsolute(file)) this.file = path.resolve(process.cwd(), file); const stats = fs.statSync(this.file); - if (!stats.isFile()) throw new Error('File path does not point to a file.'); + if (!stats.isFile()) throw new Error('CLIENT_INVALID_OPTION', 'File', 'a file'); /** * Amount of shards that this manager is going to spawn @@ -46,11 +47,11 @@ class ShardingManager extends EventEmitter { this.totalShards = options.totalShards; if (this.totalShards !== 'auto') { if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) { - throw new TypeError('Amount of shards must be a number.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); } - if (this.totalShards < 1) throw new RangeError('Amount of shards must be at least 1.'); + if (this.totalShards < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); if (this.totalShards !== Math.floor(this.totalShards)) { - throw new RangeError('Amount of shards must be an integer.'); + throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } } @@ -109,9 +110,13 @@ class ShardingManager extends EventEmitter { return this._spawn(count, delay); }); } else { - if (typeof amount !== 'number' || isNaN(amount)) throw new TypeError('Amount of shards must be a number.'); - if (amount < 1) throw new RangeError('Amount of shards must be at least 1.'); - if (amount !== Math.floor(amount)) throw new TypeError('Amount of shards must be an integer.'); + if (typeof amount !== 'number' || isNaN(amount)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); + } + if (amount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); + if (amount !== Math.floor(amount)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); + } return this._spawn(amount, delay); } } @@ -125,7 +130,7 @@ class ShardingManager extends EventEmitter { */ _spawn(amount, delay) { return new Promise(resolve => { - if (this.shards.size >= amount) throw new Error(`Already spawned ${this.shards.size} shards.`); + if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size); this.totalShards = amount; this.createShard(); @@ -181,8 +186,8 @@ class ShardingManager extends EventEmitter { * }).catch(console.error); */ fetchClientValues(prop) { - if (this.shards.size === 0) return Promise.reject(new Error('No shards have been spawned.')); - if (this.shards.size !== this.totalShards) return Promise.reject(new Error('Still spawning shards.')); + if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS')); + if (this.shards.size !== this.totalShards) return Promise.reject(new Error('SHARDING_IN_PROCESS')); const promises = []; for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop)); return Promise.all(promises); diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index cd04ad8f3..e0a2f3ba2 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -6,6 +6,7 @@ const Util = require('../util/Util'); const Guild = require('./Guild'); const Message = require('./Message'); const GroupDMChannel = require('./GroupDMChannel'); +const { TypeError } = require('../errors'); /** * Represents the logged in client's Discord user. @@ -193,7 +194,7 @@ class ClientUser extends User { } if (data.status) { - if (typeof data.status !== 'string') throw new TypeError('Status must be a string'); + if (typeof data.status !== 'string') throw new TypeError('STATUS_TYPE'); if (this.bot) { status = data.status; } else { diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 3b6f0d9ef..2dd62514a 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -14,6 +14,7 @@ const Util = require('../util/Util'); const Snowflake = require('../util/Snowflake'); const Permissions = require('../util/Permissions'); const Shared = require('./shared'); +const { Error, TypeError } = require('../errors'); /** * Represents a guild (or a server) on Discord. @@ -804,8 +805,7 @@ class Guild { */ unban(user, reason) { const id = this.client.resolver.resolveUserID(user); - if (!id) throw new Error('Couldn\'t resolve the user ID to unban.'); - + if (!id) throw new Error('BAN_RESOLVE_ID'); return this.client.api.guilds(this.id).bans(id).delete({ reason }) .then(() => user); } @@ -828,7 +828,7 @@ class Guild { * .catch(console.error); */ pruneMembers({ days = 7, dry = false, reason } = {}) { - if (typeof days !== 'number') throw new TypeError('Days must be a number.'); + if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) .then(data => data.pruned); } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 201a2b792..92969e493 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -3,6 +3,7 @@ const Role = require('./Role'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); const Presence = require('./Presence').Presence; +const { Error } = require('../errors'); /** * Represents a member of a guild on Discord. @@ -287,7 +288,7 @@ class GuildMember { */ permissionsIn(channel) { channel = this.client.resolver.resolveChannel(channel); - if (!channel || !channel.guild) throw new Error('Could not resolve channel to a guild channel.'); + if (!channel || !channel.guild) throw new Error('GUILD_CHANNEL_RESOLVE'); return channel.permissionsFor(this); } diff --git a/src/structures/Message.js b/src/structures/Message.js index 84bafec68..d4b7f001c 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -7,6 +7,7 @@ const Util = require('../util/Util'); const Collection = require('../util/Collection'); const Constants = require('../util/Constants'); const Permissions = require('../util/Permissions'); +const { TypeError } = require('../errors'); let GuildMember; /** @@ -428,7 +429,7 @@ class Message { */ react(emoji) { emoji = this.client.resolver.resolveEmojiIdentifier(emoji); - if (!emoji) throw new TypeError('Emoji must be a string or Emoji/ReactionEmoji'); + if (!emoji) throw new TypeError('EMOJI_TYPE'); return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji)['@me'] .put() diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 35d222b1c..21ef03c68 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -1,4 +1,5 @@ const Util = require('../util/Util'); +const { RangeError } = require('../errors'); /** * Represents an embed in a message (image/video preview, rich embed, etc.) @@ -165,13 +166,11 @@ class MessageEmbed { * @returns {MessageEmbed} This embed */ addField(name, value, inline = false) { - if (this.fields.length >= 25) throw new RangeError('MessageEmbeds may not exceed 25 fields.'); + if (this.fields.length >= 25) throw new RangeError('EMBED_FIELD_COUNT'); name = Util.resolveString(name); - if (name.length > 256) throw new RangeError('MessageEmbed field names may not exceed 256 characters.'); - if (!/\S/.test(name)) throw new RangeError('MessageEmbed field names may not be empty.'); + if (!String(name) || name.length > 256) throw new RangeError('EMBED_FIELD_NAME'); value = Util.resolveString(value); - if (value.length > 1024) throw new RangeError('MessageEmbed field values may not exceed 1024 characters.'); - if (!/\S/.test(value)) throw new RangeError('MessageEmbed field values may not be empty.'); + if (!String(name) || value.length > 1024) throw new RangeError('EMBED_FIELD_VALUE'); this.fields.push({ name, value, inline }); return this; } @@ -183,7 +182,7 @@ class MessageEmbed { * @returns {MessageEmbed} This embed */ attachFile(file) { - if (this.file) throw new RangeError('You may not upload more than one file at once.'); + if (this.file) throw new RangeError('EMBED_FILE_LIMIT'); this.file = file; return this; } @@ -217,7 +216,7 @@ class MessageEmbed { */ setDescription(description) { description = Util.resolveString(description); - if (description.length > 2048) throw new RangeError('MessageEmbed descriptions may not exceed 2048 characters.'); + if (description.length > 2048) throw new RangeError('EMBED_DESCRIPTION'); this.description = description; return this; } @@ -230,7 +229,7 @@ class MessageEmbed { */ setFooter(text, iconURL) { text = Util.resolveString(text); - if (text.length > 2048) throw new RangeError('MessageEmbed footer text may not exceed 2048 characters.'); + if (text.length > 2048) throw new RangeError('EMBED_FOOTER_TEXT'); this.footer = { text, iconURL }; return this; } @@ -272,7 +271,7 @@ class MessageEmbed { */ setTitle(title) { title = Util.resolveString(title); - if (title.length > 256) throw new RangeError('MessageEmbed titles may not exceed 256 characters.'); + if (title.length > 256) throw new RangeError('EMBED_TITLE'); this.title = title; return this; } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 5d4957ded..08b745dda 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -3,6 +3,7 @@ const MessageCollector = require('../MessageCollector'); const Shared = require('../shared'); const Collection = require('../../util/Collection'); const Snowflake = require('../../util/Snowflake'); +const { Error, RangeError, TypeError } = require('../../errors'); /** * Interface for classes that have text-channel-like features. @@ -136,7 +137,7 @@ class TextBasedChannel { return this.fetchMessages({ limit: 1, around: messageID }) .then(messages => { const msg = messages.get(messageID); - if (!msg) throw new Error('Message not found.'); + if (!msg) throw new Error('MESSAGE_MISSING'); return msg; }); } @@ -227,7 +228,7 @@ class TextBasedChannel { * channel.startTyping(); */ startTyping(count) { - if (typeof count !== 'undefined' && count < 1) throw new RangeError('Count must be at least 1.'); + 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, { @@ -361,7 +362,7 @@ class TextBasedChannel { }).messages ); } - throw new TypeError('The messages must be an Array, Collection, or number.'); + throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); } /** diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index 63606f5c9..76ed72eef 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -1,4 +1,5 @@ const long = require('long'); +const { TypeError } = require('../../errors'); /** * @typedef {Object} MessageSearchOptions @@ -84,9 +85,7 @@ module.exports = function search(target, options) { const Guild = require('../Guild'); const Message = require('../Message'); - if (!(target instanceof Channel || target instanceof Guild)) { - throw new TypeError('Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.'); - } + if (!(target instanceof Channel || target instanceof Guild)) throw new TypeError('SEARCH_CHANNEL_TYPE'); let endpoint = target.client.api[target instanceof Channel ? 'channels' : 'guilds'](target.id).messages().search; return endpoint.get({ query: options }).then(body => { diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index bd93e4dbe..d9e328e9d 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -1,4 +1,5 @@ const Util = require('../../util/Util'); +const { RangeError } = require('../../errors'); module.exports = function sendMessage(channel, options) { const User = require('../User'); @@ -8,7 +9,7 @@ module.exports = function sendMessage(channel, options) { if (typeof nonce !== 'undefined') { nonce = parseInt(nonce); - if (isNaN(nonce) || nonce < 0) throw new RangeError('Message nonce must fit in an unsigned 64-bit integer.'); + if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); } if (content) { diff --git a/src/util/Constants.js b/src/util/Constants.js index 98b359b4c..6da968eea 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1,4 +1,5 @@ exports.Package = require('../../package.json'); +const { Error, RangeError } = require('../errors'); /** * Options for a client. @@ -78,20 +79,6 @@ exports.WSCodes = { 4011: 'Shard would be on too many guilds if connected', }; -exports.Errors = { - NO_TOKEN: 'Request to use token, but token was unavailable to the client.', - NO_BOT_ACCOUNT: 'Only bot accounts are able to make use of this feature.', - NO_USER_ACCOUNT: 'Only user accounts are able to make use of this feature.', - BAD_WS_MESSAGE: 'A bad message was received from the websocket; either bad compression, or not JSON.', - TOOK_TOO_LONG: 'Something took too long to do.', - NOT_A_PERMISSION: 'Invalid permission string or number.', - 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.', -}; - const AllowedImageFormats = [ 'webp', 'png', @@ -108,8 +95,8 @@ const AllowedImageSizes = [ ]; function checkImage({ size, format }) { - if (format && !AllowedImageFormats.includes(format)) throw new Error(`Invalid image format: ${format}`); - if (size && !AllowedImageSizes.includes(size)) throw new RangeError(`Invalid size: ${size}`); + if (format && !AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format); + if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size); } exports.Endpoints = { diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 04b1864fd..b6dd20478 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -1,4 +1,4 @@ -const Constants = require('../util/Constants'); +const { RangeError } = require('../errors'); /** * Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of @@ -95,7 +95,7 @@ class Permissions { static resolve(permission) { if (permission instanceof Array) return permission.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); if (typeof permission === 'string') permission = this.FLAGS[permission]; - if (typeof permission !== 'number' || permission < 1) throw new RangeError(Constants.Errors.NOT_A_PERMISSION); + if (typeof permission !== 'number' || permission < 1) throw new RangeError('PERMISSION_INVALID'); return permission; } } diff --git a/src/util/Util.js b/src/util/Util.js index e48d736c8..ef61cec5f 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1,6 +1,7 @@ const snekfetch = require('snekfetch'); const Constants = require('./Constants'); const ConstantsHttp = Constants.DefaultOptions.http; +const { RangeError, TypeError } = require('../errors'); /** * Contains various general-purpose utility methods. These functions are also available on the base `Discord` object. @@ -19,7 +20,9 @@ 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 Error('Message exceeds the max length and contains no split characters.'); + if (splitText.length === 1) { + throw new RangeError('SPLIT_MAX_LEN'); + } const messages = ['']; let msg = 0; for (let i = 0; i < splitText.length; i++) { @@ -54,7 +57,7 @@ class Util { */ static fetchRecommendedShards(token, guildsPerShard = 1000) { return new Promise((resolve, reject) => { - if (!token) throw new Error('A token must be provided.'); + if (!token) throw new Error('TOKEN_MISSING'); snekfetch.get(`${ConstantsHttp.host}/api/v${ConstantsHttp.version}${Constants.Endpoints.botGateway}`) .set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`) .end((err, res) => { @@ -279,9 +282,9 @@ class Util { } if (color < 0 || color > 0xFFFFFF) { - throw new RangeError('Color must be within the range 0 - 16777215 (0xFFFFFF).'); + throw new RangeError('COLOR_RANGE'); } else if (color && isNaN(color)) { - throw new TypeError('Unable to convert color to a number.'); + throw new TypeError('COLOR_CONVERT'); } return color; From 0a337a4646c2b906a195182bddfcdf4a774fbcbf Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Sun, 25 Jun 2017 12:48:30 -0700 Subject: [PATCH 0107/1359] make token not enumerable (#1620) --- src/client/Client.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/Client.js b/src/client/Client.js index 203863451..064702253 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -133,6 +133,7 @@ class Client extends EventEmitter { */ this.presences = new Collection(); + Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'CLIENT_TOKEN' in process.env) { /** * Authorization token for the logged in user/bot From 45cc175851d1b8fc3a059159470549b5a55b242a Mon Sep 17 00:00:00 2001 From: Drahcirius Date: Tue, 27 Jun 2017 15:22:17 -0400 Subject: [PATCH 0108/1359] setTimeout should use args (#1623) --- src/client/Client.js | 4 ++-- src/client/WebhookClient.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 064702253..88d770252 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -447,9 +447,9 @@ class Client extends EventEmitter { */ setTimeout(fn, delay, ...args) { const timeout = setTimeout(() => { - fn(); + fn(...args); this._timeouts.delete(timeout); - }, delay, ...args); + }, delay); this._timeouts.add(timeout); return timeout; } diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index 36c945e01..1dc60024e 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -72,9 +72,9 @@ class WebhookClient extends Webhook { */ setTimeout(fn, delay, ...args) { const timeout = setTimeout(() => { - fn(); + fn(...args); this._timeouts.delete(timeout); - }, delay, ...args); + }, delay); this._timeouts.add(timeout); return timeout; } From ead6d80c36adbeda4a267f7984a69cbfda064106 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 28 Jun 2017 09:27:29 -0500 Subject: [PATCH 0109/1359] Fix toLowerCase errors in GuildAuditLogs (#1627) --- src/structures/GuildAuditLogs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 9dd326637..744121d23 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -2,6 +2,7 @@ const Collection = require('../util/Collection'); const Snowflake = require('../util/Snowflake'); const Targets = { + ALL: 'ALL', GUILD: 'GUILD', CHANNEL: 'CHANNEL', USER: 'USER', @@ -14,6 +15,7 @@ const Targets = { }; const Actions = { + ALL: null, GUILD_UPDATE: 1, CHANNEL_CREATE: 10, CHANNEL_UPDATE: 11, From 5891c0b4d7ecc92a474e291026054e6e8db96a23 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 28 Jun 2017 09:29:49 -0500 Subject: [PATCH 0110/1359] update tern file to actually work (#1630) --- .tern-project | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.tern-project b/.tern-project index cc31d86e5..8f37bf06e 100644 --- a/.tern-project +++ b/.tern-project @@ -1,11 +1,21 @@ { - "ecmaVersion": 6, + "ecmaVersion": 7, "libs": [], + "loadEagerly": [ + "./src/*.js" + ], + "dontLoad": [ + "node_modules/**" + ], "plugins": { - "node": { - "dontLoad": "node_modules/**", - "load": "", - "modules": "" + "es_modules": {}, + "node": {}, + "doc_comment": { + "fullDocs": true, + "strong": true + }, + "webpack": { + "configPath": "./webpack.config.js", } } } From 029efe5cb07e10b1d77c4e725400007c8c098ade Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 28 Jun 2017 19:17:57 +0200 Subject: [PATCH 0111/1359] Fixed Guild#deleteEmoji, it's now using the emoji's id (#1633) --- 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 2dd62514a..0cc5cab07 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -982,7 +982,7 @@ class Guild { */ deleteEmoji(emoji) { if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); - return this.client.api.guilds(this.id).emojis(this.id).delete() + return this.client.api.guilds(this.id).emojis(emoji.id).delete() .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } From 7b442623462f7718f65fc9f30cd2a3fb3463caa3 Mon Sep 17 00:00:00 2001 From: Gavin Wainwright Date: Wed, 28 Jun 2017 19:43:26 -0400 Subject: [PATCH 0112/1359] Destructure certain imports (#1634) --- src/client/Client.js | 4 ++-- src/client/voice/VoiceConnection.js | 2 +- src/client/voice/VoiceUDPClient.js | 2 +- src/client/voice/VoiceWebSocket.js | 2 +- src/client/voice/receiver/VoiceReadable.js | 2 +- src/client/voice/receiver/VoiceReceiver.js | 2 +- src/client/websocket/WebSocketManager.js | 2 +- src/sharding/ShardingManager.js | 2 +- src/structures/Guild.js | 2 +- src/structures/GuildMember.js | 2 +- src/structures/User.js | 2 +- src/structures/interfaces/Collector.js | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 88d770252..dac538593 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,5 +1,5 @@ const os = require('os'); -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); const Constants = require('../util/Constants'); const Permissions = require('../util/Permissions'); const Util = require('../util/Util'); @@ -11,7 +11,7 @@ const ClientVoiceManager = require('./voice/ClientVoiceManager'); const WebSocketManager = require('./websocket/WebSocketManager'); const ActionsManager = require('./actions/ActionsManager'); const Collection = require('../util/Collection'); -const Presence = require('../structures/Presence').Presence; +const { Presence } = require('../structures/Presence'); const VoiceRegion = require('../structures/VoiceRegion'); const Webhook = require('../structures/Webhook'); const User = require('../structures/User'); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 0c4af468c..562d4211d 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -4,7 +4,7 @@ const Util = require('../../util/Util'); const Constants = require('../../util/Constants'); const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/VoiceReceiver'); -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); const Prism = require('prism-media'); const { Error } = require('../../errors'); diff --git a/src/client/voice/VoiceUDPClient.js b/src/client/voice/VoiceUDPClient.js index 6e12d5e7b..04245f60b 100644 --- a/src/client/voice/VoiceUDPClient.js +++ b/src/client/voice/VoiceUDPClient.js @@ -1,7 +1,7 @@ const udp = require('dgram'); const dns = require('dns'); const Constants = require('../../util/Constants'); -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); const { Error } = require('../../errors'); /** diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index 025c23ecc..4184a22ad 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -1,6 +1,6 @@ const Constants = require('../../util/Constants'); const SecretKey = require('./util/SecretKey'); -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); const { Error } = require('../../errors'); let WebSocket; diff --git a/src/client/voice/receiver/VoiceReadable.js b/src/client/voice/receiver/VoiceReadable.js index d34942802..b29d37aad 100644 --- a/src/client/voice/receiver/VoiceReadable.js +++ b/src/client/voice/receiver/VoiceReadable.js @@ -1,4 +1,4 @@ -const Readable = require('stream').Readable; +const { Readable } = require('stream'); class VoiceReadable extends Readable { constructor() { diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index e57a39d9c..6f2f5722b 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -1,4 +1,4 @@ -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); const secretbox = require('../util/Secretbox'); const Readable = require('./VoiceReadable'); const OpusEncoders = require('../opus/OpusEngineList'); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index aabea1d9b..8f120b975 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,4 +1,4 @@ -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); const Constants = require('../../util/Constants'); const WebSocketConnection = require('./WebSocketConnection'); diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index bfae42f22..8c16096e7 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -1,6 +1,6 @@ const path = require('path'); const fs = require('fs'); -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); const Shard = require('./Shard'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 0cc5cab07..491d3d5ea 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,7 @@ const Emoji = require('./Emoji'); const Invite = require('./Invite'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); -const Presence = require('./Presence').Presence; +const { Presence } = require('./Presence'); const GuildMember = require('./GuildMember'); const VoiceRegion = require('./VoiceRegion'); const Constants = require('../util/Constants'); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 92969e493..4474e0a0c 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -2,7 +2,7 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Role = require('./Role'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); -const Presence = require('./Presence').Presence; +const { Presence } = require('./Presence'); const { Error } = require('../errors'); /** diff --git a/src/structures/User.js b/src/structures/User.js index 0b85875a2..d01ebd138 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -1,6 +1,6 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Constants = require('../util/Constants'); -const Presence = require('./Presence').Presence; +const { Presence } = require('./Presence'); const UserProfile = require('./UserProfile'); const Snowflake = require('../util/Snowflake'); diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 287256ebe..77fe507c0 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -1,5 +1,5 @@ const Collection = require('../../util/Collection'); -const EventEmitter = require('events').EventEmitter; +const EventEmitter = require('events'); /** * Filter to be applied to the collector. From 6bc7b3e0689b727e29cc67cc87bc9c631454f4e0 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 1 Jul 2017 04:11:15 -0500 Subject: [PATCH 0113/1359] add user account checkbox (#1640) --- .github/ISSUE_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b368eeb5c..125a143fb 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -19,6 +19,7 @@ This issue tracker is only for bug reports and enhancement suggestions. You won' - Operating system: - Priority this issue should have – please be realistic and elaborate if possible: +- [ ] I found this issue while running code on a __user account__ +- [ ] I found this issue while running code on a __user account__ - [ ] I have also tested the issue on latest master, commit hash: From 7f69c62ddc65500895a196d4d7a8fee9dba226a5 Mon Sep 17 00:00:00 2001 From: Robin B Date: Mon, 17 Jul 2017 12:03:40 +0200 Subject: [PATCH 0145/1359] Fix docs for Application secret type (#1690) Now a string (as supposed to be) instead of boolean. --- src/structures/OAuth2Application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/OAuth2Application.js b/src/structures/OAuth2Application.js index a4d8875c8..15297f60e 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/OAuth2Application.js @@ -86,7 +86,7 @@ class OAuth2Application { /** * OAuth2 secret for the application - * @type {boolean} + * @type {string} */ this.secret = data.secret; From edfd50d003a7168000ed10b01f8e3817d58439ca Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 18 Jul 2017 17:11:09 +0200 Subject: [PATCH 0146/1359] afkChannelID returns a Snowflake rather than an string (#1697) little update to .afkChannelID property in docs --- 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 d4436dc0b..1e3dd20d0 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -136,7 +136,7 @@ class Guild { /** * The ID of the voice channel where AFK members are moved - * @type {?string} + * @type {?Snowflake} */ this.afkChannelID = data.afk_channel_id; From 8cf95dc9b3388958b50eee76da432c4e67c9279c Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 20 Jul 2017 19:11:12 -0500 Subject: [PATCH 0147/1359] update fetch member to work in a more consistent way (#1696) * Update Guild.js * Update Guild.js --- src/structures/Guild.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1e3dd20d0..5a11de42c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -499,14 +499,15 @@ class Guild { /** * Fetches all the members in the guild, even if they are offline. If the guild has less than 250 members, * this should not be necessary. - * @param {string} [query=''] Limit fetch to members with similar usernames - * @param {number} [limit=0] Maximum number of members to request + * @param {Object} [options] Options for the fetch operation + * @param {string} [options.query=''] Limit fetch to members with similar usernames + * @param {number} [options.limit=0] Maximum number of members to request * @returns {Promise>} */ - fetchMembers(query = '', limit = 0) { + fetchMembers({ query = '', limit = 0 }) { return new Promise((resolve, reject) => { if (this.memberCount === this.members.size) { - resolve(new Collection()); + resolve((query || limit) ? new Collection() : this.members); return; } this.client.ws.send({ @@ -520,17 +521,19 @@ class Guild { const fetchedMembers = new Collection(); const handler = (members, guild) => { if (guild.id !== this.id) return; - for (const member of members.values()) fetchedMembers.set(member.user.id, member); + for (const member of members.values()) { + if (query || limit) fetchedMembers.set(member.user.id, member); + } if (this.memberCount === this.members.size || ((query || limit) && members.size < 1000)) { this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); - resolve(fetchedMembers); + resolve((query || limit) ? fetchedMembers : this.members); } }; this.client.on(Constants.Events.GUILD_MEMBERS_CHUNK, handler); this.client.setTimeout(() => { this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); reject(new Error('Members didn\'t arrive in time.')); - }, 120 * 1000); + }, 120e3); }); } From 7a27b12b2b5ecc4e31c4fb58020acc47b297cf51 Mon Sep 17 00:00:00 2001 From: Raphael Date: Fri, 21 Jul 2017 02:11:37 +0200 Subject: [PATCH 0148/1359] .applicationID and .ownerID return a Snowflake rather than a string (#1700) --- src/structures/GroupDMChannel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index ba417143b..8c16257b4 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -54,7 +54,7 @@ class GroupDMChannel extends Channel { /** * The user ID of this Group DM's owner - * @type {string} + * @type {Snowflake} */ this.ownerID = data.owner_id; @@ -66,7 +66,7 @@ class GroupDMChannel extends Channel { /** * Application ID of the application that made this Group DM, if applicable - * @type {?string} + * @type {?Snowflake} */ this.applicationID = data.application_id; From 11556c0b3b0317341da49875d072f732a054f2f1 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 21 Jul 2017 02:27:19 +0200 Subject: [PATCH 0149/1359] Moved all error strings to src/errors/messages and a few other things (#1695) * Added missing error messages As well as `Guild#setRolePosition` and `Guild#setChannelPosition`'s first arg validation And fixed a typo in `Guild#setChannelPosition` `roles` -> `channels` * Reverted collection and Util constructors * Removed leftover messages Should have been in the second commit. * It's a single invalid permission and removed unused flag error * Fix INVALID_TOKEN -> TOKEN_INVALID as of #1703 --- src/client/ClientManager.js | 2 +- src/errors/Messages.js | 21 +++++++++++++++++---- src/structures/ClientUserSettings.js | 5 +++-- src/structures/Guild.js | 23 +++++++++++++---------- src/structures/GuildChannel.js | 3 ++- src/structures/GuildMember.js | 6 +++--- src/structures/Message.js | 4 ++-- src/structures/MessageReaction.js | 3 ++- src/structures/User.js | 3 ++- src/structures/VoiceChannel.js | 3 ++- src/util/Util.js | 4 ++-- 11 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 4b7f0102b..3ffc82e7c 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -38,7 +38,7 @@ class ClientManager { connectToWebSocket(token, resolve, reject) { this.client.emit(Constants.Events.DEBUG, `Authenticated using token ${token}`); this.client.token = token; - const timeout = this.client.setTimeout(() => reject(new Error('INVALID_TOKEN')), 1000 * 300); + const timeout = this.client.setTimeout(() => reject(new Error('TOKEN_INVALID')), 1000 * 300); this.client.api.gateway.get().then(res => { const protocolVersion = Constants.DefaultOptions.ws.version; const gateway = `${res.url}/?v=${protocolVersion}&encoding=${WebSocketConnection.ENCODING}`; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index bd0c10a32..eda968dcb 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -13,8 +13,7 @@ const Messages = { WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, - PERMISSIONS_INVALID: 'Invalid permission string or number.', - PERMISSIONS_INVALID_FLAG: 'Invalid bitfield flag string or number', + PERMISSION_INVALID: 'Invalid permission string or number.', RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.', @@ -26,6 +25,8 @@ const Messages = { SHARDING_IN_PROCESS: 'Shards are still being spawned', SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards`, + SHARD_MESSAGE_FAILED: 'Failed to send message to master process.', + COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', COLOR_CONVERT: 'Unable to convert color to a number.', @@ -39,7 +40,8 @@ const Messages = { FILE_NOT_FOUND: file => `File could not be found: ${file}`, USER_STATUS: 'User status must be a string', - SHARD_MESSAGE_FAILED: 'Failed to send message to master process.', + USER_NOT_CACHED: 'User is not cached. Use Client.fetchUser first.', + USER_NO_DMCHANNEL: 'No DM Channel exists!', VOICE_INVALID_HEARTBEAT: 'Tried to set voice heartbeat but no valid interval was specified.', VOICE_USER_MISSING: 'Couldn\'t resolve the user to create stream.', @@ -50,6 +52,7 @@ const Messages = { VOICE_TOKEN_ABSENT: 'Token not provided from voice server packet.', VOICE_SESSION_ABSENT: 'Session ID not supplied.', VOICE_INVALID_ENDPOINT: 'Invalid endpoint received.', + VOICE_NO_BROWSER: 'Voice connections are not available in browsers.', OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', @@ -71,7 +74,7 @@ const Messages = { SPLIT_MAX_LEN: 'Message exceeds the max length and contains no split characters.', - BAN_RESOLVE_ID: 'Couldn\'t resolve the user ID to unban.', + BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user ID to ${ban ? 'ban' : 'unban'}.`, PRUNE_DAYS_TYPE: 'Days must be a number', @@ -80,8 +83,18 @@ const Messages = { MESSAGE_SPLIT_MISSING: 'Message exceeds the max length and contains no split characters.', GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', + GUILD_OWNED: 'Guild is owned by the client.', + GUILD_RESTRICTED: (state = false) => `Guild is ${state ? 'already' : 'not'} restricted.`, + GUILD_MEMBERS_TIMEOUT: 'Members didn\'t arrive in time.', + + INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, + + + WEBHOOK_MESSAGE: 'The message was not sent by a webhook.', EMOJI_TYPE: 'Emoji must be a string or Emoji/ReactionEmoji', + + REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.', }; for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/structures/ClientUserSettings.js b/src/structures/ClientUserSettings.js index 33cb1d733..67466f951 100644 --- a/src/structures/ClientUserSettings.js +++ b/src/structures/ClientUserSettings.js @@ -1,5 +1,6 @@ const Constants = require('../util/Constants'); const Util = require('../util/Util'); +const { Error } = require('../errors'); /** * A wrapper around the ClientUser's settings. @@ -54,7 +55,7 @@ class ClientUserSettings { */ addRestrictedGuild(guild) { const temp = Object.assign([], this.restrictedGuilds); - if (temp.includes(guild.id)) return Promise.reject(new Error('Guild is already restricted')); + if (temp.includes(guild.id)) return Promise.reject(new Error('GUILD_RESTRICTED', true)); temp.push(guild.id); return this.update('restricted_guilds', temp).then(() => guild); } @@ -67,7 +68,7 @@ class ClientUserSettings { removeRestrictedGuild(guild) { const temp = Object.assign([], this.restrictedGuilds); const index = temp.indexOf(guild.id); - if (index < 0) return Promise.reject(new Error('Guild is not restricted')); + if (index < 0) return Promise.reject(new Error('GUILD_RESTRICTED')); temp.splice(index, 1); return this.update('restricted_guilds', temp).then(() => guild); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 5a11de42c..991907180 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -6,6 +6,7 @@ const Invite = require('./Invite'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const { Presence } = require('./Presence'); +const GuildChannel = require('./GuildChannel'); const GuildMember = require('./GuildMember'); const VoiceRegion = require('./VoiceRegion'); const Constants = require('../util/Constants'); @@ -487,7 +488,7 @@ class Guild { */ fetchMember(user, cache = true) { user = this.client.resolver.resolveUser(user); - if (!user) return Promise.reject(new Error('User is not cached. Use Client.fetchUser first.')); + if (!user) return Promise.reject(new Error('USER_NOT_CACHED')); if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); return this.client.api.guilds[this.id].members[user.id].get() .then(data => { @@ -532,7 +533,7 @@ class Guild { this.client.on(Constants.Events.GUILD_MEMBERS_CHUNK, handler); this.client.setTimeout(() => { this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); - reject(new Error('Members didn\'t arrive in time.')); + reject(new Error('GUILD_MEMBERS_TIMEOUT')); }, 120e3); }); } @@ -730,7 +731,7 @@ class Guild { */ setPosition(position, relative) { if (this.client.user.bot) { - return Promise.reject(new Error('Setting guild position is only available for user accounts')); + return Promise.reject(new Error('FEATURE_USER_ONLY')); } return this.client.user.settings.setGuildPosition(this, position, relative); } @@ -779,7 +780,7 @@ class Guild { ban(user, options = { days: 0 }) { if (options.days) options['delete-message-days'] = options.days; const id = this.client.resolver.resolveUserID(user); - if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); + if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true)); return this.client.api.guilds[this.id].bans[id].put({ query: options }) .then(() => { if (user instanceof GuildMember) return user; @@ -996,7 +997,7 @@ class Guild { * .catch(console.error); */ leave() { - if (this.ownerID === this.client.user.id) return Promise.reject(new Error('Guild is owned by the client.')); + if (this.ownerID === this.client.user.id) return Promise.reject(new Error('GUILD_OWNED')); return this.client.api.users['@me'].guilds[this.id].delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } @@ -1158,11 +1159,11 @@ class Guild { setRolePosition(role, position, relative = false) { if (typeof role === 'string') { role = this.roles.get(role); - if (!role) return Promise.reject(new Error('Supplied role is not a role or snowflake.')); } + if (!(role instanceof Role)) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); position = Number(position); - if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + if (isNaN(position)) return Promise.reject(new TypeError('INVALID_TYPE', 'position', 'number')); let updatedRoles = this._sortedRoles.array(); @@ -1188,11 +1189,13 @@ class Guild { setChannelPosition(channel, position, relative = false) { if (typeof channel === 'string') { channel = this.channels.get(channel); - if (!channel) return Promise.reject(new Error('Supplied channel is not a channel or snowflake.')); + } + if (!(channel instanceof GuildChannel)) { + return Promise.reject(new TypeError('INVALID_TYPE', 'channel', 'GuildChannel nor a Snowflake')); } position = Number(position); - if (isNaN(position)) return Promise.reject(new Error('Supplied position is not a number.')); + if (isNaN(position)) return Promise.reject(new TypeError('INVALID_TYPE', 'position', 'number')); let updatedChannels = this._sortedChannels(channel.type).array(); @@ -1203,7 +1206,7 @@ class Guild { .then(() => this.client.actions.GuildChannelsPositionUpdate.handle({ guild_id: this.id, - roles: updatedChannels, + channels: updatedChannels, }).guild ); } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index ad0d8e0d2..ff2e33a58 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -4,6 +4,7 @@ const Invite = require('./Invite'); const PermissionOverwrites = require('./PermissionOverwrites'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); +const { TypeError } = require('../errors'); /** * Represents a guild channel (i.e. text channels and voice channels). @@ -163,7 +164,7 @@ class GuildChannel extends Channel { } else { userOrRole = this.client.resolver.resolveUser(userOrRole); payload.type = 'member'; - if (!userOrRole) return Promise.reject(new TypeError('Supplied parameter was neither a User nor a Role.')); + if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true)); } payload.id = userOrRole.id; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8ffea8627..af0cd7b70 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -3,7 +3,7 @@ const Role = require('./Role'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); const { Presence } = require('./Presence'); -const { Error } = require('../errors'); +const { Error, TypeError } = require('../errors'); /** * Represents a member of a guild on Discord. @@ -396,7 +396,7 @@ class GuildMember { */ addRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); - if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); + if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); if (this._roles.includes(role.id)) return Promise.resolve(this); return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] .put() @@ -426,7 +426,7 @@ class GuildMember { */ removeRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); - if (!role) return Promise.reject(new TypeError('Supplied parameter was neither a Role nor a Snowflake.')); + if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] .delete() .then(() => this); diff --git a/src/structures/Message.js b/src/structures/Message.js index 4ef37ddec..2c737e5c2 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -7,7 +7,7 @@ const Util = require('../util/Util'); const Collection = require('../util/Collection'); const Constants = require('../util/Constants'); const Permissions = require('../util/Permissions'); -const { TypeError } = require('../errors'); +const { Error, TypeError } = require('../errors'); let GuildMember; /** @@ -519,7 +519,7 @@ class Message { * @returns {Promise} */ fetchWebhook() { - if (!this.webhookID) return Promise.reject(new Error('The message was not sent by a webhook.')); + if (!this.webhookID) return Promise.reject(new Error('WEBHOOK_MESSAGE')); return this.client.fetchWebhook(this.webhookID); } diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 3ac5f4e84..ee00bbf0d 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,6 +1,7 @@ const Collection = require('../util/Collection'); const Emoji = require('./Emoji'); const ReactionEmoji = require('./ReactionEmoji'); +const { Error } = require('../errors'); /** * Represents a reaction to a message. @@ -62,7 +63,7 @@ class MessageReaction { */ remove(user = this.message.client.user) { const userID = this.message.client.resolver.resolveUserID(user); - if (!userID) return Promise.reject(new Error('Couldn\'t resolve the user ID to remove from the reaction.')); + if (!userID) return Promise.reject(new Error('REACTION_RESOLVE_USER')); return this.message.client.api.channels[this.message.channel.id].messages[this.message.id] .reactions[this.emoji.identifier][userID === this.message.client.user.id ? '@me' : userID] .delete() diff --git a/src/structures/User.js b/src/structures/User.js index 8550344b3..e3d2e0b4c 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -3,6 +3,7 @@ const Constants = require('../util/Constants'); const { Presence } = require('./Presence'); const UserProfile = require('./UserProfile'); const Snowflake = require('../util/Snowflake'); +const { Error } = require('../errors'); /** * Represents a user on Discord. @@ -212,7 +213,7 @@ class User { * @returns {Promise} */ deleteDM() { - if (!this.dmChannel) return Promise.reject(new Error('No DM Channel exists!')); + if (!this.dmChannel) return Promise.reject(new Error('USER_NO_DMCHANNEL')); return this.client.api.channels[this.dmChannel.id].delete() .then(data => this.client.actions.ChannelDelete.handle(data).channel); } diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 617b4eee0..7de964ec6 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,5 +1,6 @@ const GuildChannel = require('./GuildChannel'); const Collection = require('../util/Collection'); +const { Error } = require('../errors'); /** * Represents a guild voice channel on Discord. @@ -113,7 +114,7 @@ class VoiceChannel extends GuildChannel { * .catch(console.error); */ join() { - if (this.client.browser) return Promise.reject(new Error('Voice connections are not available in browsers.')); + if (this.client.browser) return Promise.reject(new Error('VOICE_NO_BROWSER')); return this.client.voice.joinChannel(this); } diff --git a/src/util/Util.js b/src/util/Util.js index 121ecf4f8..2023050d0 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1,7 +1,7 @@ const snekfetch = require('snekfetch'); const Constants = require('./Constants'); const ConstantsHttp = Constants.DefaultOptions.http; -const { RangeError, TypeError } = require('../errors'); +const { Error: DiscordError, RangeError, TypeError } = require('../errors'); const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k); /** @@ -58,7 +58,7 @@ class Util { */ static fetchRecommendedShards(token, guildsPerShard = 1000) { return new Promise((resolve, reject) => { - if (!token) throw new Error('TOKEN_MISSING'); + if (!token) throw new DiscordError('TOKEN_MISSING'); snekfetch.get(`${ConstantsHttp.api}/v${ConstantsHttp.version}${Constants.Endpoints.botGateway}`) .set('Authorization', `Bot ${token.replace(/^Bot\s*/i, '')}`) .end((err, res) => { From a2eeafc75de02cf1651a89c91164657ec6e15aca Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 20 Jul 2017 19:32:40 -0500 Subject: [PATCH 0150/1359] rewrite ratelimiting and api route builder (#1667) * rewrite ratelimiting and api route builder * more stuff * let people pass their own handlers * Update burst.js * Update RequestHandler.js * Update burst.js * Update sequential.js * Update RequestHandler.js --- src/client/Client.js | 8 +- src/client/rest/APIRequest.js | 12 +-- src/client/rest/APIRouter.js | 40 ++++---- src/client/rest/RESTManager.js | 23 ++--- src/client/rest/RequestHandlers/Burst.js | 65 ------------ .../rest/RequestHandlers/RequestHandler.js | 54 ---------- src/client/rest/RequestHandlers/Sequential.js | 98 ------------------- src/client/rest/handlers/RequestHandler.js | 67 +++++++++++++ src/client/rest/handlers/burst.js | 13 +++ src/client/rest/handlers/index.js | 5 + src/client/rest/handlers/sequential.js | 16 +++ src/structures/Channel.js | 2 +- src/structures/ClientUser.js | 6 +- src/structures/Emoji.js | 2 +- src/structures/Guild.js | 40 ++++---- src/structures/GuildChannel.js | 8 +- src/structures/GuildMember.js | 14 +-- src/structures/Message.js | 12 +-- src/structures/User.js | 8 +- src/structures/Webhook.js | 10 +- 20 files changed, 187 insertions(+), 316 deletions(-) delete mode 100644 src/client/rest/RequestHandlers/Burst.js delete mode 100644 src/client/rest/RequestHandlers/RequestHandler.js delete mode 100644 src/client/rest/RequestHandlers/Sequential.js create mode 100644 src/client/rest/handlers/RequestHandler.js create mode 100644 src/client/rest/handlers/burst.js create mode 100644 src/client/rest/handlers/index.js create mode 100644 src/client/rest/handlers/sequential.js diff --git a/src/client/Client.js b/src/client/Client.js index 5831500d6..114174a43 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -332,7 +332,7 @@ class Client extends EventEmitter { */ fetchUser(id, cache = true) { if (this.users.has(id)) return Promise.resolve(this.users.get(id)); - return this.api.users[id].get().then(data => + return this.api.users(id).get().then(data => cache ? this.dataManager.newUser(data) : new User(this, data) ); } @@ -344,7 +344,7 @@ class Client extends EventEmitter { */ fetchInvite(invite) { const code = this.resolver.resolveInviteCode(invite); - return this.api.invites[code].get({ query: { with_counts: true } }) + return this.api.invites(code).get({ query: { with_counts: true } }) .then(data => new Invite(this, data)); } @@ -355,7 +355,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchWebhook(id, token) { - return this.api.webhooks.opts(id, token).get().then(data => new Webhook(this, data)); + return this.api.webhooks(id, token).get().then(data => new Webhook(this, data)); } /** @@ -414,7 +414,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchApplication(id = '@me') { - return this.api.oauth2.applications[id].get() + return this.api.oauth2.applications(id).get() .then(app => new OAuth2Application(this, app)); } diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 9382c9ae2..b8914a6d2 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -8,20 +8,10 @@ class APIRequest { this.client = rest.client; this.method = method; this.path = path.toString(); - this.route = this.getRoute(this.path); + this.route = options.route; this.options = options; } - 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() { if (this.client.token && this.client.user && this.client.user.bot) { return `Bot ${this.client.token}`; diff --git a/src/client/rest/APIRouter.js b/src/client/rest/APIRouter.js index e9771e902..88103305d 100644 --- a/src/client/rest/APIRouter.js +++ b/src/client/rest/APIRouter.js @@ -1,32 +1,34 @@ const util = require('util'); +const noop = () => {}; // eslint-disable-line no-empty-function const methods = ['get', 'post', 'delete', 'patch', 'put']; const reflectors = [ 'toString', 'valueOf', 'inspect', 'constructor', Symbol.toPrimitive, util.inspect.custom, ]; -module.exports = restManager => { +function buildRoute(manager) { + const route = ['']; const handler = { - get(list, name) { - if (name === 'opts') { - function toReturn(...args) { // eslint-disable-line no-inner-declarations - list.push(...args.filter(x => x !== null && typeof x !== 'undefined')); - return new Proxy(list, handler); - } - const directJoin = () => `${list.join('/')}/${name}`; - for (const r of reflectors) toReturn[r] = directJoin; - for (const method of methods) { - toReturn[method] = options => restManager.request(method, directJoin(), options); - } - return toReturn; + get(target, name) { + if (reflectors.includes(name)) return () => route.join('/'); + if (methods.includes(name)) { + return options => manager.request(name, route.join('/'), Object.assign({ + route: route.map((r, i) => { + if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id'; + return r; + }).join('/'), + }, options)); } - if (reflectors.includes(name)) return () => list.join('/'); - if (methods.includes(name)) return options => restManager.request(name, list.join('/'), options); - list.push(name); - return new Proxy(list, handler); + route.push(name); + return new Proxy(noop, handler); + }, + apply(target, _, args) { + route.push(...args.filter(x => x != null)); // eslint-disable-line eqeqeq + return new Proxy(noop, handler); }, }; + return new Proxy(noop, handler); +} - return new Proxy([''], handler); -}; +module.exports = buildRoute; diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 70c53629f..530623245 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -1,8 +1,7 @@ const UserAgentManager = require('./UserAgentManager'); -const SequentialRequestHandler = require('./RequestHandlers/Sequential'); -const BurstRequestHandler = require('./RequestHandlers/Burst'); +const handlers = require('./handlers'); const APIRequest = require('./APIRequest'); -const mountApi = require('./APIRouter'); +const routeBuilder = require('./APIRouter'); const { Error } = require('../../errors'); class RESTManager { @@ -15,7 +14,7 @@ class RESTManager { } get api() { - return mountApi(this); + return routeBuilder(this); } destroy() { @@ -35,21 +34,17 @@ class RESTManager { } getRequestHandler() { - switch (this.client.options.apiRequestMethod) { - case 'sequential': - return SequentialRequestHandler; - case 'burst': - return BurstRequestHandler; - default: - throw new Error('RATELIMIT_INVALID_METHOD'); - } + const method = this.client.options.apiRequestMethod; + if (typeof method === 'function') return method; + const handler = handlers[method]; + if (!handler) throw new Error('RATELIMIT_INVALID_METHOD'); + return handler; } request(method, url, options = {}) { const apiRequest = new APIRequest(this, method, url, options); if (!this.handlers[apiRequest.route]) { - const RequestHandlerType = this.getRequestHandler(); - this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route); + this.handlers[apiRequest.route] = new handlers.RequestHandler(this, this.getRequestHandler()); } return this.push(this.handlers[apiRequest.route], apiRequest); diff --git a/src/client/rest/RequestHandlers/Burst.js b/src/client/rest/RequestHandlers/Burst.js deleted file mode 100644 index f5968857f..000000000 --- a/src/client/rest/RequestHandlers/Burst.js +++ /dev/null @@ -1,65 +0,0 @@ -const RequestHandler = require('./RequestHandler'); -const DiscordAPIError = require('../DiscordAPIError'); - -class BurstRequestHandler extends RequestHandler { - constructor(restManager, endpoint) { - super(restManager, endpoint); - - this.client = restManager.client; - - this.limit = Infinity; - this.resetTime = null; - this.remaining = 1; - this.timeDifference = 0; - - this.resetTimeout = null; - } - - push(request) { - super.push(request); - this.handle(); - } - - execute(item) { - if (!item) return; - item.request.gen().end((err, res) => { - if (res && res.headers) { - this.limit = Number(res.headers['x-ratelimit-limit']); - this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; - this.remaining = Number(res.headers['x-ratelimit-remaining']); - this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); - } - if (err) { - if (err.status === 429) { - this.queue.unshift(item); - if (res.headers['x-ratelimit-global']) this.globalLimit = true; - if (this.resetTimeout) return; - this.resetTimeout = this.client.setTimeout(() => { - this.remaining = this.limit; - this.globalLimit = false; - this.handle(); - this.resetTimeout = null; - }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); - } else { - item.reject(err.status === 400 ? new DiscordAPIError(res.request.path, res.body) : err); - this.handle(); - } - } else { - this.globalLimit = false; - const data = res && res.body ? res.body : {}; - item.resolve(data); - this.handle(); - } - }); - } - - handle() { - super.handle(); - if (this.remaining <= 0 || this.queue.length === 0 || this.globalLimit) return; - this.execute(this.queue.shift()); - this.remaining--; - this.handle(); - } -} - -module.exports = BurstRequestHandler; diff --git a/src/client/rest/RequestHandlers/RequestHandler.js b/src/client/rest/RequestHandlers/RequestHandler.js deleted file mode 100644 index c5bad20ab..000000000 --- a/src/client/rest/RequestHandlers/RequestHandler.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * A base class for different types of rate limiting handlers for the REST API. - * @private - */ -class RequestHandler { - /** - * @param {RESTManager} restManager The REST manager to use - */ - constructor(restManager) { - /** - * The RESTManager that instantiated this RequestHandler - * @type {RESTManager} - */ - this.restManager = restManager; - - /** - * A list of requests that have yet to be processed - * @type {APIRequest[]} - */ - this.queue = []; - } - - /** - * Whether or not the client is being rate limited on every endpoint - * @type {boolean} - * @readonly - */ - get globalLimit() { - return this.restManager.globallyRateLimited; - } - - set globalLimit(value) { - this.restManager.globallyRateLimited = value; - } - - /** - * Push a new API request into this bucket. - * @param {APIRequest} request The new request to push into the queue - */ - push(request) { - this.queue.push(request); - } - - /** - * Attempts to get this RequestHandler to process its current queue. - */ - handle() {} // eslint-disable-line no-empty-function - - destroy() { - this.queue = []; - } -} - -module.exports = RequestHandler; diff --git a/src/client/rest/RequestHandlers/Sequential.js b/src/client/rest/RequestHandlers/Sequential.js deleted file mode 100644 index 107d7a670..000000000 --- a/src/client/rest/RequestHandlers/Sequential.js +++ /dev/null @@ -1,98 +0,0 @@ -const RequestHandler = require('./RequestHandler'); -const DiscordAPIError = require('../DiscordAPIError'); - -/** - * Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto - * the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account, - * but it can be slower. - * @extends {RequestHandler} - * @private - */ -class SequentialRequestHandler extends RequestHandler { - /** - * @param {RESTManager} restManager The REST manager to use - * @param {string} endpoint The endpoint to handle - */ - constructor(restManager, endpoint) { - super(restManager, endpoint); - - /** - * The endpoint that this handler is handling - * @type {string} - */ - this.endpoint = endpoint; - - /** - * The time difference between Discord's Dates and the local computer's Dates. A positive number means the local - * computer's time is ahead of Discord's - * @type {number} - */ - this.timeDifference = 0; - - /** - * Whether the queue is being processed or not - * @type {boolean} - */ - this.busy = false; - } - - push(request) { - super.push(request); - this.handle(); - } - - /** - * Performs a request then resolves a promise to indicate its readiness for a new request. - * @param {APIRequest} item The item to execute - * @returns {Promise} - */ - execute(item) { - this.busy = true; - return new Promise(resolve => { - item.request.gen().end((err, res) => { - if (res && res.headers) { - this.requestLimit = Number(res.headers['x-ratelimit-limit']); - this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000; - this.requestRemaining = Number(res.headers['x-ratelimit-remaining']); - this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); - } - if (err) { - if (err.status === 429) { - this.queue.unshift(item); - this.restManager.client.setTimeout(() => { - this.globalLimit = false; - resolve(); - }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset); - if (res.headers['x-ratelimit-global']) this.globalLimit = true; - } else { - item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); - resolve(err); - } - } else { - this.globalLimit = false; - const data = res && res.body ? res.body : {}; - item.resolve(data); - if (this.requestRemaining === 0) { - this.restManager.client.setTimeout( - () => resolve(data), - this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset - ); - } else { - resolve(data); - } - } - }); - }); - } - - handle() { - super.handle(); - if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return; - this.execute(this.queue.shift()).then(() => { - this.busy = false; - this.handle(); - }); - } -} - -module.exports = SequentialRequestHandler; diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js new file mode 100644 index 000000000..e5dfcebe9 --- /dev/null +++ b/src/client/rest/handlers/RequestHandler.js @@ -0,0 +1,67 @@ +const DiscordAPIError = require('../DiscordAPIError'); + +class RequestHandler { + constructor(manager, handler) { + this.manager = manager; + this.client = this.manager.client; + this.handle = handler.bind(this); + this.limit = Infinity; + this.resetTime = null; + this.remaining = 1; + this.timeDifference = 0; + + this.queue = []; + } + + get limited() { + return this.queue.length === 0 || this.manager.globallyRateLimited || this.remaining <= 0; + } + + set globallyLimited(limited) { + this.manager.globallyRateLimited = limited; + } + + push(request) { + this.queue.push(request); + this.handle(); + } + + execute(item) { + return new Promise((resolve, reject) => { + const finish = timeout => { + // eslint-disable-next-line prefer-promise-reject-errors + if (timeout || this.limited) reject({ timeout, limited: this.limited }); + else resolve(); + }; + item.request.gen().end((err, res) => { + if (res && res.headers) { + if (res.headers['x-ratelimit-global']) this.globallyLimited = true; + this.limit = Number(res.headers['x-ratelimit-limit']); + this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.remaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else { + item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); + finish(); + } + } else { + const data = res && res.body ? res.body : {}; + item.resolve(data); + finish(); + } + }); + }); + } + + reset() { + this.globallyLimited = false; + this.remaining = 1; + } +} + +module.exports = RequestHandler; diff --git a/src/client/rest/handlers/burst.js b/src/client/rest/handlers/burst.js new file mode 100644 index 000000000..b4e4fd2e3 --- /dev/null +++ b/src/client/rest/handlers/burst.js @@ -0,0 +1,13 @@ +module.exports = function burst() { + if (this.limited) return; + this.execute(this.queue.shift()) + .then(this.handle.bind(this)) + .catch(({ timeout }) => { + this.client.setTimeout(() => { + this.reset(); + this.handle(); + }, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset)); + }); + this.remaining--; + this.handle(); +}; diff --git a/src/client/rest/handlers/index.js b/src/client/rest/handlers/index.js new file mode 100644 index 000000000..47792c46c --- /dev/null +++ b/src/client/rest/handlers/index.js @@ -0,0 +1,5 @@ +module.exports = { + sequential: require('./sequential'), + burst: require('./burst'), + RequestHandler: require('./RequestHandler'), +}; diff --git a/src/client/rest/handlers/sequential.js b/src/client/rest/handlers/sequential.js new file mode 100644 index 000000000..1f644dc17 --- /dev/null +++ b/src/client/rest/handlers/sequential.js @@ -0,0 +1,16 @@ +module.exports = function sequential() { + if (this.busy || this.limited) return; + this.busy = true; + this.execute(this.queue.shift()) + .then(() => { + this.busy = false; + this.handle(); + }) + .catch(({ timeout }) => { + this.client.setTimeout(() => { + this.reset(); + this.busy = false; + this.handle(); + }, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset)); + }); +}; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 463179649..d8f877c9a 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -62,7 +62,7 @@ class Channel { * .catch(console.error); // Log error */ delete() { - return this.client.api.channels[this.id].delete().then(() => this); + return this.client.api.channels(this.id).delete().then(() => this); } } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 4d26487ed..e0a2f3ba2 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -91,7 +91,7 @@ class ClientUser extends User { if (data.new_password) _data.new_password = data.newPassword; } - return this.client.api.users['@me'].patch({ data }) + return this.client.api.users('@me').patch({ data }) .then(newData => this.client.actions.UserUpdate.handle(newData).updated); } @@ -284,7 +284,7 @@ class ClientUser extends User { if (options.guild instanceof Guild) options.guild = options.guild.id; Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - return this.client.api.users['@me'].mentions.get({ query: options }) + return this.client.api.users('@me').mentions.get({ query: options }) .then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); } @@ -351,7 +351,7 @@ class ClientUser extends User { return o; }, {}), } : { recipients: recipients.map(u => this.client.resolver.resolveUserID(u)) }; - return this.client.api.users['@me'].channels.post({ data }) + return this.client.api.users('@me').channels.post({ data }) .then(res => new GroupDMChannel(this.client, res)); } } diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index fc3ab07e9..3010d578b 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -121,7 +121,7 @@ class Emoji { * .catch(console.error); */ edit(data, reason) { - return this.client.api.guilds[this.guild.id].emojis[this.id] + return this.client.api.guilds(this.guild.id).emojis(this.id) .patch({ data: { name: data.name, roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : [], diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 991907180..f4ef6dd86 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -378,7 +378,7 @@ class Guild { * @returns {Promise>} */ fetchBans() { - return this.client.api.guilds[this.id].bans.get().then(bans => + return this.client.api.guilds(this.id).bans.get().then(bans => bans.reduce((collection, ban) => { collection.set(ban.user.id, { reason: ban.reason, @@ -394,7 +394,7 @@ class Guild { * @returns {Promise>} */ fetchInvites() { - return this.client.api.guilds[this.id].invites.get() + return this.client.api.guilds(this.id).invites.get() .then(inviteItems => { const invites = new Collection(); for (const inviteItem of inviteItems) { @@ -410,7 +410,7 @@ class Guild { * @returns {Promise>} */ fetchWebhooks() { - return this.client.api.guilds[this.id].webhooks.get().then(data => { + return this.client.api.guilds(this.id).webhooks.get().then(data => { const hooks = new Collection(); for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); return hooks; @@ -422,7 +422,7 @@ class Guild { * @returns {Promise>} */ fetchVoiceRegions() { - return this.client.api.guilds[this.id].regions.get().then(res => { + return this.client.api.guilds(this.id).regions.get().then(res => { const regions = new Collection(); for (const region of res) regions.set(region.id, new VoiceRegion(region)); return regions; @@ -444,7 +444,7 @@ class Guild { if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; - return this.client.api.guilds[this.id]['audit-logs'].get({ query: { + return this.client.api.guilds(this.id)['audit-logs'].get({ query: { before: options.before, after: options.after, limit: options.limit, @@ -476,7 +476,7 @@ class Guild { options.roles = roles.map(role => role.id); } } - return this.client.api.guilds[this.id].members[user.id].put({ data: options }) + return this.client.api.guilds(this.id).members(user.id).put({ data: options }) .then(data => this.client.actions.GuildMemberGet.handle(this, data).member); } @@ -490,7 +490,7 @@ class Guild { user = this.client.resolver.resolveUser(user); if (!user) return Promise.reject(new Error('USER_NOT_CACHED')); if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); - return this.client.api.guilds[this.id].members[user.id].get() + return this.client.api.guilds(this.id).members(user.id).get() .then(data => { if (cache) return this.client.actions.GuildMemberGet.handle(this, data).member; else return new GuildMember(this, data); @@ -597,7 +597,7 @@ class Guild { if (typeof data.explicitContentFilter !== 'undefined') { _data.explicit_content_filter = Number(data.explicitContentFilter); } - return this.client.api.guilds[this.id].patch({ data: _data, reason }) + return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } @@ -742,7 +742,7 @@ class Guild { * @returns {Promise} */ acknowledge() { - return this.client.api.guilds[this.id].ack + return this.client.api.guilds(this.id).ack .post({ data: { token: this.client.rest._ackToken } }) .then(res => { if (res.token) this.client.rest._ackToken = res.token; @@ -781,7 +781,7 @@ class Guild { if (options.days) options['delete-message-days'] = options.days; const id = this.client.resolver.resolveUserID(user); if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true)); - return this.client.api.guilds[this.id].bans[id].put({ query: options }) + return this.client.api.guilds(this.id).bans[id].put({ query: options }) .then(() => { if (user instanceof GuildMember) return user; const _user = this.client.resolver.resolveUser(id); @@ -830,7 +830,7 @@ class Guild { */ pruneMembers({ days = 7, dry = false, reason } = {}) { if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); - return this.client.api.guilds[this.id].prune[dry ? 'get' : 'post']({ query: { days }, reason }) + return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) .then(data => data.pruned); } @@ -865,7 +865,7 @@ class Guild { id: overwrite.id, })); } - return this.client.api.guilds[this.id].channels.post({ + return this.client.api.guilds(this.id).channels.post({ data: { name, type, permission_overwrites: overwrites, }, @@ -898,7 +898,7 @@ class Guild { }; } - return this.client.api.guilds[this.id].channels.patch({ data: { + return this.client.api.guilds(this.id).channels.patch({ data: { guild_id: this.id, channels: channelPositions, } }).then(() => @@ -936,7 +936,7 @@ class Guild { if (data.color) data.color = Util.resolveColor(data.color); if (data.permissions) data.permissions = Permissions.resolve(data.permissions); - return this.client.api.guilds[this.id].roles.post({ data, reason }).then(role => + return this.client.api.guilds(this.id).roles.post({ data, reason }).then(role => this.client.actions.GuildRoleCreate.handle({ guild_id: this.id, role, @@ -965,7 +965,7 @@ class Guild { if (typeof attachment === 'string' && attachment.startsWith('data:')) { const data = { image: attachment, name }; if (roles) data.roles = roles.map(r => r.id ? r.id : r); - return this.client.api.guilds[this.id].emojis.post({ data }) + return this.client.api.guilds(this.id).emojis.post({ data }) .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); } else { return this.client.resolver.resolveBuffer(attachment) @@ -983,7 +983,7 @@ class Guild { */ deleteEmoji(emoji) { if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); - return this.client.api.guilds(this.id).emojis[emoji.id].delete() + return this.client.api.guilds(this.id).emojis(emoji.id).delete() .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } @@ -998,7 +998,7 @@ class Guild { */ leave() { if (this.ownerID === this.client.user.id) return Promise.reject(new Error('GUILD_OWNED')); - return this.client.api.users['@me'].guilds[this.id].delete() + return this.client.api.users('@me').guilds(this.id).delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } @@ -1012,7 +1012,7 @@ class Guild { * .catch(console.error); */ delete() { - return this.client.api.guilds[this.id].delete() + return this.client.api.guilds(this.id).delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } @@ -1170,7 +1170,7 @@ class Guild { Util.moveElementInArray(updatedRoles, role, position, relative); updatedRoles = updatedRoles.map((r, i) => ({ id: r.id, position: i })); - return this.client.api.guilds[this.id].roles.patch({ data: updatedRoles }) + return this.client.api.guilds(this.id).roles.patch({ data: updatedRoles }) .then(() => this.client.actions.GuildRolesPositionUpdate.handle({ guild_id: this.id, @@ -1202,7 +1202,7 @@ class Guild { Util.moveElementInArray(updatedChannels, channel, position, relative); updatedChannels = updatedChannels.map((r, i) => ({ id: r.id, position: i })); - return this.client.api.guilds[this.id].channels.patch({ data: updatedChannels }) + return this.client.api.guilds(this.id).channels.patch({ data: updatedChannels }) .then(() => this.client.actions.GuildChannelsPositionUpdate.handle({ guild_id: this.id, diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index ff2e33a58..7d405c564 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -189,7 +189,7 @@ class GuildChannel extends Channel { } } - return this.client.api.channels[this.id].permissions[payload.id] + return this.client.api.channels(this.id).permissions[payload.id] .put({ data: payload, reason }) .then(() => this); } @@ -216,7 +216,7 @@ class GuildChannel extends Channel { * .catch(console.error); */ edit(data, reason) { - return this.client.api.channels[this.id].patch({ + return this.client.api.channels(this.id).patch({ data: { name: (data.name || this.name).trim(), topic: data.topic || this.topic, @@ -289,7 +289,7 @@ class GuildChannel extends Channel { * @returns {Promise} */ createInvite({ temporary = false, maxAge = 86400, maxUses = 0, unique, reason } = {}) { - return this.client.api.channels[this.id].invites.post({ data: { + return this.client.api.channels(this.id).invites.post({ data: { temporary, max_age: maxAge, max_uses: maxUses, unique, }, reason }) .then(invite => new Invite(this.client, invite)); @@ -353,7 +353,7 @@ class GuildChannel extends Channel { * .catch(console.error); // Log error */ delete(reason) { - return this.client.api.channels[this.id].delete({ reason }).then(() => this); + return this.client.api.channels(this.id).delete({ reason }).then(() => this); } /** diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index af0cd7b70..f533a75ce 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -342,13 +342,13 @@ class GuildMember { data.channel = null; } if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); - let endpoint = this.client.api.guilds[this.guild.id]; + let endpoint = this.client.api.guilds(this.guild.id); if (this.user.id === this.client.user.id) { const keys = Object.keys(data); - if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members['@me'].nick; - else endpoint = endpoint.members[this.id]; + if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me').nick; + else endpoint = endpoint.members(this.id); } else { - endpoint = endpoint.members[this.id]; + endpoint = endpoint.members(this.id); } return endpoint.patch({ data, reason }).then(newData => this.guild._updateMember(this, newData).mem); } @@ -398,7 +398,7 @@ class GuildMember { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); if (this._roles.includes(role.id)) return Promise.resolve(this); - return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) .put() .then(() => this); } @@ -427,7 +427,7 @@ class GuildMember { removeRole(role) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); - return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) .delete() .then(() => this); } @@ -484,7 +484,7 @@ class GuildMember { * @returns {Promise} */ kick(reason) { - return this.client.api.guilds[this.guild.id].members[this.user.id].delete({ reason }) + return this.client.api.guilds(this.guild.id).members(this.user.id).delete({ reason }) .then(() => this.client.actions.GuildMemberRemove.handle({ guild_id: this.guild.id, diff --git a/src/structures/Message.js b/src/structures/Message.js index 2c737e5c2..75ae5d7eb 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -413,7 +413,7 @@ class Message { * @returns {Promise} */ pin() { - return this.client.api.channels[this.channel.id].pins[this.id].put() + return this.client.api.channels(this.channel.id).pins(this.id).put() .then(() => this); } @@ -422,7 +422,7 @@ class Message { * @returns {Promise} */ unpin() { - return this.client.api.channels[this.channel.id].pins[this.id].delete() + return this.client.api.channels(this.channel.id).pins(this.id).delete() .then(() => this); } @@ -435,7 +435,7 @@ class Message { emoji = this.client.resolver.resolveEmojiIdentifier(emoji); if (!emoji) throw new TypeError('EMOJI_TYPE'); - return this.client.api.channels[this.channel.id].messages[this.id].reactions[emoji]['@me'] + return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji, '@me') .put() .then(() => this._addReaction(Util.parseEmoji(emoji), this.client.user)); } @@ -445,7 +445,7 @@ class Message { * @returns {Promise} */ clearReactions() { - return this.client.api.channels[this.channel.id].messages[this.id].reactions.delete() + return this.client.api.channels(this.channel.id).messages(this.id).reactions.delete() .then(() => this); } @@ -463,7 +463,7 @@ class Message { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.client.api.channels[this.channel.id].messages[this.id] + return this.client.api.channels(this.channel.id).messages(this.id) .delete({ reason }) .then(() => this.client.actions.MessageDelete.handle({ @@ -506,7 +506,7 @@ class Message { * @returns {Promise} */ acknowledge() { - return this.client.api.channels[this.channel.id].messages[this.id].ack + return this.client.api.channels(this.channel.id).messages(this.id).ack .post({ data: { token: this.client.rest._ackToken } }) .then(res => { if (res.token) this.client.rest._ackToken = res.token; diff --git a/src/structures/User.js b/src/structures/User.js index e3d2e0b4c..022f51f60 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -202,7 +202,7 @@ class User { */ createDM() { if (this.dmChannel) return Promise.resolve(this.dmChannel); - return this.client.api.users[this.client.user.id].channels.post({ data: { + return this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }) .then(data => this.client.actions.ChannelCreate.handle(data).channel); @@ -214,7 +214,7 @@ class User { */ deleteDM() { if (!this.dmChannel) return Promise.reject(new Error('USER_NO_DMCHANNEL')); - return this.client.api.channels[this.dmChannel.id].delete() + return this.client.api.channels(this.dmChannel.id).delete() .then(data => this.client.actions.ChannelDelete.handle(data).channel); } @@ -224,7 +224,7 @@ class User { * @returns {Promise} */ fetchProfile() { - return this.client.api.users[this.id].profile.get().then(data => new UserProfile(this, data)); + return this.client.api.users(this.id).profile.get().then(data => new UserProfile(this, data)); } /** @@ -234,7 +234,7 @@ class User { * @returns {Promise} */ setNote(note) { - return this.client.api.users['@me'].notes[this.id].put({ data: { note } }) + return this.client.api.users('@me').notes(this.id).put({ data: { note } }) .then(() => this); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 20af8554e..f1ee54fad 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -153,7 +153,7 @@ class Webhook { file.file = buffer; return file; }) - )).then(files => this.client.api.webhooks.opts(this.id, this.token).post({ + )).then(files => this.client.api.webhooks(this.id, this.token).post({ data: options, query: { wait: true }, files, @@ -161,7 +161,7 @@ class Webhook { })); } - return this.client.api.webhooks.opts(this.id, this.token).post({ + return this.client.api.webhooks(this.id, this.token).post({ data: options, query: { wait: true }, auth: false, @@ -190,7 +190,7 @@ class Webhook { * }).catch(console.error); */ sendSlackMessage(body) { - return this.client.api.webhooks.opts(this.id, this.token).slack.post({ + return this.client.api.webhooks(this.id, this.token).slack.post({ query: { wait: true }, auth: false, data: body, @@ -216,7 +216,7 @@ class Webhook { return this.edit({ name, avatar: dataURI }, reason); }); } - return this.client.api.webhooks.opts(this.id, this.token).patch({ + return this.client.api.webhooks(this.id, this.token).patch({ data: { name, avatar }, reason, }).then(data => { @@ -232,7 +232,7 @@ class Webhook { * @returns {Promise} */ delete(reason) { - return this.client.api.webhooks.opts(this.id, this.token).delete({ reason }); + return this.client.api.webhooks(this.id, this.token).delete({ reason }); } } From aeb8f85106bdbe491488363e84cc8cb23742bcd3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 21 Jul 2017 15:00:20 +0200 Subject: [PATCH 0151/1359] Guild#fetchMember's options paremeter should be optional (#1705) --- 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 f4ef6dd86..1c82869ba 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -505,7 +505,7 @@ class Guild { * @param {number} [options.limit=0] Maximum number of members to request * @returns {Promise>} */ - fetchMembers({ query = '', limit = 0 }) { + fetchMembers({ query = '', limit = 0 } = {}) { return new Promise((resolve, reject) => { if (this.memberCount === this.members.size) { resolve((query || limit) ? new Collection() : this.members); From 57b69803132355b506f9919f37a55915ccdf86d3 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 22 Jul 2017 16:21:50 -0500 Subject: [PATCH 0152/1359] retry on 500 (#1709) --- src/client/rest/handlers/RequestHandler.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js index e5dfcebe9..d89c5b4a0 100644 --- a/src/client/rest/handlers/RequestHandler.js +++ b/src/client/rest/handlers/RequestHandler.js @@ -45,6 +45,9 @@ class RequestHandler { if (err.status === 429) { this.queue.unshift(item); finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else if (err.status === 500) { + this.queue.unshift(item); + finish(1e3 + this.client.options.restTimeOffset); } else { item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); finish(); From 278fe74a5859e1d5743e344586648278cb9755dd Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 26 Jul 2017 10:04:25 +0200 Subject: [PATCH 0153/1359] Adding reason parameter to GuildMember's methods (#1710) * Added reason parameter to GuildMember's methods * Reason parameters are optional --- src/structures/GuildMember.js | 40 +++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index f533a75ce..f6a10cbe9 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -356,19 +356,21 @@ class GuildMember { /** * Mute/unmute a user. * @param {boolean} mute Whether or not the member should be muted + * @param {string} [reason] Reason for muting or unmuting * @returns {Promise} */ - setMute(mute) { - return this.edit({ mute }); + setMute(mute, reason) { + return this.edit({ mute }, reason); } /** * Deafen/undeafen a user. * @param {boolean} deaf Whether or not the member should be deafened + * @param {string} [reason] Reason for deafening or undeafening * @returns {Promise} */ - setDeaf(deaf) { - return this.edit({ deaf }); + setDeaf(deaf, reason) { + return this.edit({ deaf }, reason); } /** @@ -383,32 +385,35 @@ class GuildMember { /** * Sets the roles applied to the member. * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to apply + * @param {string} [reason] Reason for applying the roles * @returns {Promise} */ - setRoles(roles) { - return this.edit({ roles }); + setRoles(roles, reason) { + return this.edit({ roles }, reason); } /** * Adds a single role to the member. * @param {Role|Snowflake} role The role or ID of the role to add + * @param {string} [reason] Reason for adding the role * @returns {Promise} */ - addRole(role) { + addRole(role, reason) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); if (this._roles.includes(role.id)) return Promise.resolve(this); return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) - .put() + .put({ reason }) .then(() => this); } /** * Adds multiple roles to the member. * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to add + * @param {string} [reason] Reason for adding the roles * @returns {Promise} */ - addRoles(roles) { + addRoles(roles, reason) { let allRoles; if (roles instanceof Collection) { allRoles = this._roles.slice(); @@ -416,28 +421,30 @@ class GuildMember { } else { allRoles = this._roles.concat(roles.map(r => r.id ? r.id : r)); } - return this.edit({ roles: allRoles }); + return this.edit({ roles: allRoles }, reason); } /** * Removes a single role from the member. * @param {Role|Snowflake} role The role or ID of the role to remove + * @param {string} [reason] Reason for removing the role * @returns {Promise} */ - removeRole(role) { + removeRole(role, reason) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) - .delete() + .delete({ reason }) .then(() => this); } /** * Removes multiple roles from the member. * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to remove + * @param {string} [reason] Reason for removing the roles * @returns {Promise} */ - removeRoles(roles) { + removeRoles(roles, reason) { const allRoles = this._roles.slice(); if (roles instanceof Collection) { for (const role of roles.values()) { @@ -450,16 +457,17 @@ class GuildMember { if (index >= 0) allRoles.splice(index, 1); } } - return this.edit({ roles: allRoles }); + return this.edit({ roles: allRoles }, reason); } /** * Set the nickname for the guild member. * @param {string} nick The nickname for the guild member + * @param {string} [reason] Reason for setting the nickname * @returns {Promise} */ - setNickname(nick) { - return this.edit({ nick }); + setNickname(nick, reason) { + return this.edit({ nick }, reason); } /** From 7eb9e65c410f0a6cae7a31daa12abb6081bf5d57 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Wed, 26 Jul 2017 01:04:43 -0700 Subject: [PATCH 0154/1359] check guild availability when aggregating client emojis (#1711) --- src/client/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index 114174a43..69986e071 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -245,7 +245,7 @@ class Client extends EventEmitter { get emojis() { const emojis = new Collection(); for (const guild of this.guilds.values()) { - for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji); + if (guild.available) for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji); } return emojis; } From 4342ed29a845a7caf1b5f22b40c5e1d2d491600a Mon Sep 17 00:00:00 2001 From: aemino Date: Wed, 26 Jul 2017 01:06:40 -0700 Subject: [PATCH 0155/1359] Audio bitrate support (#1439) * Audio bitrate support Note: not implemented for VoiceBroadcasts * Fix default args, auto bitrate * Late night typos are the best * Changes bitrate to kbps for VoiceChannel stuff * Add methods to manipulate bitrate while encoding --- src/client/voice/VoiceBroadcast.js | 34 +++++++------- src/client/voice/VoiceConnection.js | 11 +++-- .../voice/dispatcher/StreamDispatcher.js | 10 +++++ src/client/voice/opus/BaseOpusEngine.js | 27 +++++++++--- src/client/voice/opus/NodeOpusEngine.js | 6 ++- src/client/voice/opus/OpusEngineList.js | 8 +--- src/client/voice/opus/OpusScriptEngine.js | 6 ++- src/client/voice/player/AudioPlayer.js | 44 ++++++++++++------- src/structures/VoiceChannel.js | 9 ++-- 9 files changed, 97 insertions(+), 58 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index c4562f17f..76513d86a 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -143,8 +143,8 @@ class VoiceBroadcast extends VolumeInterface { * }) * .catch(console.error); */ - playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { - const options = { seek, volume, passes, stream }; + playStream(stream, options = {}) { + this.setVolume(options.volume || 1); return this._playTranscodable(stream, options); } @@ -164,19 +164,17 @@ class VoiceBroadcast extends VolumeInterface { * }) * .catch(console.error); */ - playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) { - const options = { seek, volume, passes }; + playFile(file, options = {}) { + this.setVolume(options.volume || 1); return this._playTranscodable(`file:${file}`, options); } _playTranscodable(media, options) { - OpusEncoders.guaranteeOpusEngine(); - this.killCurrentTranscoder(); const transcoder = this.prism.transcode({ type: 'ffmpeg', media, - ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek)]), + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), }); /** * Emitted whenever an error occurs. @@ -206,31 +204,28 @@ class VoiceBroadcast extends VolumeInterface { } /** - * Plays a stream of 16-bit signed stereo PCM at 48KHz. + * Plays a stream of 16-bit signed stereo PCM. * @param {ReadableStream} stream The audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {VoiceBroadcast} */ - playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { - OpusEncoders.guaranteeOpusEngine(); - + playConvertedStream(stream, options = {}) { this.killCurrentTranscoder(); - const options = { seek, volume, passes, stream }; - this.currentTranscoder = { options }; + this.setVolume(options.volume || 1); + this.currentTranscoder = { options: { stream } }; stream.once('readable', () => this._startPlaying()); return this; } /** - * Plays an Opus encoded stream at 48KHz. + * Plays an Opus encoded stream. * Note that inline volume is not compatible with this method. * @param {ReadableStream} stream The Opus audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} */ - playOpusStream(stream, { seek = 0, passes = 1 } = {}) { - const options = { seek, passes, stream }; - this.currentTranscoder = { options, opus: true }; + playOpusStream(stream) { + this.currentTranscoder = { options: { stream }, opus: true }; stream.once('readable', () => this._startPlaying()); return this; } @@ -241,8 +236,9 @@ class VoiceBroadcast extends VolumeInterface { * @param {StreamOptions} [options] Options for playing the stream * @returns {VoiceBroadcast} */ - playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) { - const options = { seek, volume, passes, input }; + playArbitraryInput(input, options = {}) { + this.setVolume(options.volume || 1); + options.input = input; return this._playTranscodable(input, options); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index f4ed1b16f..68b2b0fbb 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -432,6 +432,8 @@ class VoiceConnection extends EventEmitter { * @property {number} [seek=0] The time to seek to * @property {number} [volume=1] The volume to play at * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss + * @property {number|string} [bitrate=48000] The bitrate (quality) of the audio. + * If set to 'auto', the voice channel's bitrate will be used */ /** @@ -482,7 +484,7 @@ class VoiceConnection extends EventEmitter { } /** - * Plays a stream of 16-bit signed stereo PCM at 48KHz. + * Plays a stream of 16-bit signed stereo PCM. * @param {ReadableStream} stream The audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} @@ -492,7 +494,7 @@ class VoiceConnection extends EventEmitter { } /** - * Plays an Opus encoded stream at 48KHz. + * Plays an Opus encoded stream. * Note that inline volume is not compatible with this method. * @param {ReadableStream} stream The Opus audio stream to play * @param {StreamOptions} [options] Options for playing the stream @@ -505,6 +507,7 @@ class VoiceConnection extends EventEmitter { /** * Plays a voice broadcast. * @param {VoiceBroadcast} broadcast The broadcast to play + * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} * @example * // Play a broadcast @@ -513,8 +516,8 @@ class VoiceConnection extends EventEmitter { * .playFile('./test.mp3'); * const dispatcher = voiceConnection.playBroadcast(broadcast); */ - playBroadcast(broadcast) { - return this.player.playBroadcast(broadcast); + playBroadcast(broadcast, options) { + return this.player.playBroadcast(broadcast, options); } /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 68c83d380..24df9d069 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -119,6 +119,16 @@ class StreamDispatcher extends VolumeInterface { this.emit('speaking', value); } + + /** + * Set the bitrate of the current Opus encoder. + * @param {number} bitrate New bitrate, in kbps. + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(bitrate) { + this.player.setBitrate(bitrate); + } + sendBuffer(buffer, sequence, timestamp, opusPacket) { opusPacket = opusPacket || this.player.opusEncoder.encode(buffer); const packet = this.createPacket(sequence, timestamp, opusPacket); diff --git a/src/client/voice/opus/BaseOpusEngine.js b/src/client/voice/opus/BaseOpusEngine.js index b63b695f6..40c6204fd 100644 --- a/src/client/voice/opus/BaseOpusEngine.js +++ b/src/client/voice/opus/BaseOpusEngine.js @@ -4,21 +4,38 @@ */ class BaseOpus { /** - * @param {Object} [options] The options to apply to the Opus engine - * @param {boolean} [options.fec] Whether to enable forward error correction (defaults to false) - * @param {number} [options.plp] The expected packet loss percentage (0-1 inclusive, defaults to 0) + * @param {Object} [options] The options to apply to the Opus engine. + * @param {number} [options.bitrate=48] The desired bitrate (kbps). + * @param {boolean} [options.fec=false] Whether to enable forward error correction. + * @param {number} [options.plp=0] The expected packet loss percentage. */ - constructor(options = {}) { + constructor({ bitrate = 48, fec = false, plp = 0 } = {}) { this.ctl = { + BITRATE: 4002, FEC: 4012, PLP: 4014, }; - this.options = options; + this.samplingRate = 48000; + this.channels = 2; + + /** + * The desired bitrate (kbps) + * @type {number} + */ + this.bitrate = bitrate; + + /** + * Miscellaneous Opus options + * @type {Object} + */ + this.options = { fec, plp }; } init() { try { + this.setBitrate(this.bitrate); + // Set FEC (forward error correction) if (this.options.fec) this.setFEC(this.options.fec); diff --git a/src/client/voice/opus/NodeOpusEngine.js b/src/client/voice/opus/NodeOpusEngine.js index 72f2a818b..02e880637 100644 --- a/src/client/voice/opus/NodeOpusEngine.js +++ b/src/client/voice/opus/NodeOpusEngine.js @@ -10,10 +10,14 @@ class NodeOpusEngine extends OpusEngine { } catch (err) { throw err; } - this.encoder = new opus.OpusEncoder(48000, 2); + this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels); super.init(); } + setBitrate(bitrate) { + this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + setFEC(enabled) { this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0); } diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js index 9ff9ad7bf..01e3ff6d1 100644 --- a/src/client/voice/opus/OpusEngineList.js +++ b/src/client/voice/opus/OpusEngineList.js @@ -5,8 +5,6 @@ const list = [ require('./OpusScriptEngine'), ]; -let opusEngineFound; - function fetch(Encoder, engineOptions) { try { return new Encoder(engineOptions); @@ -27,10 +25,6 @@ exports.fetch = engineOptions => { const fetched = fetch(encoder, engineOptions); if (fetched) return fetched; } - return null; -}; -exports.guaranteeOpusEngine = () => { - if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch()); - if (!opusEngineFound) throw new Error('OPUS_ENGINE_MISSING'); + throw new Error('OPUS_ENGINE_MISSING'); }; diff --git a/src/client/voice/opus/OpusScriptEngine.js b/src/client/voice/opus/OpusScriptEngine.js index 81ca6206b..a5e046d40 100644 --- a/src/client/voice/opus/OpusScriptEngine.js +++ b/src/client/voice/opus/OpusScriptEngine.js @@ -10,10 +10,14 @@ class OpusScriptEngine extends OpusEngine { } catch (err) { throw err; } - this.encoder = new OpusScript(48000, 2); + this.encoder = new OpusScript(this.samplingRate, this.channels); super.init(); } + setBitrate(bitrate) { + this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + setFEC(enabled) { this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0); } diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index afbc9a671..2a264f9e6 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -30,11 +30,6 @@ class AudioPlayer extends EventEmitter { * @type {Prism} */ this.prism = new Prism(); - /** - * The opus encoder that the player uses - * @type {NodeOpusEngine|OpusScriptEngine} - */ - this.opusEncoder = OpusEncoders.fetch(); this.streams = new Collection(); this.currentStream = {}; this.streamingData = { @@ -67,6 +62,7 @@ class AudioPlayer extends EventEmitter { destroy() { if (this.opusEncoder) this.opusEncoder.destroy(); + this.opusEncoder = null; } destroyCurrentStream() { @@ -83,13 +79,25 @@ class AudioPlayer extends EventEmitter { this.currentStream = {}; } - playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { - OpusEncoders.guaranteeOpusEngine(); - const options = { seek, volume, passes }; + /** + * Set the bitrate of the current Opus encoder. + * @param {number} value New bitrate, in kbps. + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(value) { + if (!value) return; + if (!this.opusEncoder) return; + const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value; + this.opusEncoder.setBitrate(bitrate); + } + + playUnknownStream(stream, options = {}) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); const transcoder = this.prism.transcode({ type: 'ffmpeg', media: stream, - ffmpegArguments: ffmpegArguments.concat(['-ss', String(seek)]), + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), }); this.destroyCurrentStream(); this.currentStream = { @@ -105,9 +113,10 @@ class AudioPlayer extends EventEmitter { return this.playPCMStream(transcoder.output, options, true); } - playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}, fromUnknown = false) { - OpusEncoders.guaranteeOpusEngine(); - const options = { seek, volume, passes }; + playPCMStream(stream, options = {}, fromUnknown = false) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); + this.setBitrate(options.bitrate); const dispatcher = this.createDispatcher(stream, options); if (fromUnknown) { this.currentStream.dispatcher = dispatcher; @@ -122,8 +131,8 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - playOpusStream(stream, { seek = 0, passes = 1 } = {}) { - const options = { seek, passes, opus: true }; + playOpusStream(stream, options = {}) { + options.opus = true; this.destroyCurrentStream(); const dispatcher = this.createDispatcher(stream, options); this.currentStream = { @@ -134,8 +143,7 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - playBroadcast(broadcast, { volume = 1, passes = 1 } = {}) { - const options = { volume, passes }; + playBroadcast(broadcast, options) { this.destroyCurrentStream(); const dispatcher = this.createDispatcher(broadcast, options); this.currentStream = { @@ -148,7 +156,9 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - createDispatcher(stream, options) { + createDispatcher(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + const dispatcher = new StreamDispatcher(this, stream, options); dispatcher.on('end', () => this.destroyCurrentStream()); dispatcher.on('error', () => this.destroyCurrentStream()); diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 7de964ec6..0afa6e4c2 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -26,7 +26,7 @@ class VoiceChannel extends GuildChannel { * The bitrate of this voice channel * @type {number} */ - this.bitrate = data.bitrate; + this.bitrate = data.bitrate * 0.001; /** * The maximum amount of users allowed in this channel - 0 means unlimited. @@ -77,16 +77,17 @@ class VoiceChannel extends GuildChannel { } /** - * Sets the bitrate of the channel. + * Sets the bitrate of the channel (in kbps). * @param {number} bitrate The new bitrate * @returns {Promise} * @example * // Set the bitrate of a voice channel - * voiceChannel.setBitrate(48000) - * .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`)) + * voiceChannel.setBitrate(48) + * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`)) * .catch(console.error); */ setBitrate(bitrate) { + bitrate *= 1000; return this.edit({ bitrate }); } From e29a3ec08b743ca62e4cc9b27aa18abf837d0bd0 Mon Sep 17 00:00:00 2001 From: aemino Date: Wed, 26 Jul 2017 01:10:21 -0700 Subject: [PATCH 0156/1359] Handle unexpected disconnects via packets (#1521) --- src/client/voice/ClientVoiceManager.js | 10 +++++++--- src/client/voice/VoiceConnection.js | 12 +++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 400a752cc..daeceb83a 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -1,4 +1,5 @@ const Collection = require('../../util/Collection'); +const Constants = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); const { Error } = require('../../errors'); @@ -31,10 +32,13 @@ class ClientVoiceManager { onVoiceStateUpdate({ guild_id, session_id, channel_id }) { const connection = this.connections.get(guild_id); - if (connection) { - connection.channel = this.client.channels.get(channel_id); - connection.setSessionID(session_id); + if (!connection) return; + if (!channel_id && connection.status !== Constants.VoiceStatus.DISCONNECTED) { + connection._disconnect(); + return; } + connection.channel = this.client.channels.get(channel_id); + connection.setSessionID(session_id); } /** diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 68b2b0fbb..d15c8ffbc 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -307,7 +307,14 @@ class VoiceConnection extends EventEmitter { this.sendVoiceStateUpdate({ channel_id: null, }); - this.player.destroy(); + this._disconnect(); + } + + /** + * Internally disconnects (doesn't send disconnect packet.) + * @private + */ + _disconnect() { this.cleanup(); this.status = Constants.VoiceStatus.DISCONNECTED; /** @@ -317,11 +324,14 @@ class VoiceConnection extends EventEmitter { this.emit('disconnect'); } + /** * Cleans up after disconnect. * @private */ cleanup() { + this.player.destroy(); + const { ws, udp } = this.sockets; if (ws) { From 8bd7b8211020476e9d74befd814486ddb6fcd7f7 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Wed, 26 Jul 2017 01:10:35 -0700 Subject: [PATCH 0157/1359] update collectors (#1616) * start new collector stuff * bugfixes * remove pointless cleanup method * rename methods, events, and options; remove extraneous methods, * update doc ref --- src/structures/MessageCollector.js | 39 ++++++++---- src/structures/ReactionCollector.js | 62 ++++++++++++++----- src/structures/interfaces/Collector.js | 85 +++++++++++++++++--------- 3 files changed, 130 insertions(+), 56 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 0a0a8d405..8a049f2db 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -31,16 +31,28 @@ class MessageCollector extends Collector { */ this.received = 0; - this.client.on('message', this.listener); + const bulkDeleteListener = (messages => { + for (const message of messages.values()) this.handleDispose(message); + }).bind(this); + + this.client.on('message', this.handleCollect); + this.client.on('messageDelete', this.handleDispose); + this.client.on('messageDeleteBulk', bulkDeleteListener); + + this.once('end', () => { + this.client.removeListener('message', this.handleCollect); + this.client.removeListener('messageDelete', this.handleDispose); + this.client.removeListener('messageDeleteBulk', bulkDeleteListener); + }); } /** - * Handle an incoming message for possible collection. + * Handle a message for possible collection. * @param {Message} message The message that could be collected * @returns {?{key: Snowflake, value: Message}} Message data to collect * @private */ - handle(message) { + collect(message) { if (message.channel.id !== this.channel.id) return null; this.received++; return { @@ -50,23 +62,24 @@ class MessageCollector extends Collector { } /** - * Check after collection to see if the collector is done. + * Handle a message for possible disposal. + * @param {Message} message The message that could be disposed + * @returns {?string} The message ID. + */ + dispose(message) { + return message.channel.id === this.channel.id ? message.id : null; + } + + /** + * Check after un/collection to see if the collector is done. * @returns {?string} Reason to end the collector, if any * @private */ - postCheck() { + endReason() { if (this.options.max && this.collected.size >= this.options.max) return 'limit'; if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; return null; } - - /** - * Removes event listeners. - * @private - */ - cleanup() { - this.client.removeListener('message', this.listener); - } } module.exports = MessageCollector; diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index a54687d32..ba4eef4de 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -39,7 +39,27 @@ class ReactionCollector extends Collector { */ this.total = 0; - this.client.on('messageReactionAdd', this.listener); + this.empty = this.empty.bind(this); + + this.client.on('messageReactionAdd', this.handleCollect); + this.client.on('messageReactionRemove', this.handleDispose); + this.client.on('messageReactionRemoveAll', this.empty); + + this.once('end', () => { + this.client.removeListener('messageReactionAdd', this.handleCollect); + this.client.removeListener('messageReactionRemove', this.handleDispose); + this.client.removeListener('messageReactionRemoveAll', this.empty); + }); + + this.on('collect', (collected, reaction, user) => { + this.total++; + this.users.set(user.id, user); + }); + + this.on('dispose', (disposed, reaction, user) => { + this.total--; + if (!this.collected.some(r => r.users.has(user.id))) this.users.delete(user.id); + }); } /** @@ -48,35 +68,47 @@ class ReactionCollector extends Collector { * @returns {?{key: Snowflake, value: MessageReaction}} Reaction data to collect * @private */ - handle(reaction) { + collect(reaction) { if (reaction.message.id !== this.message.id) return null; return { - key: reaction.emoji.id || reaction.emoji.name, + key: ReactionCollector.key(reaction), value: reaction, }; } /** - * Check after collection to see if the collector is done. - * @param {MessageReaction} reaction The reaction that was collected - * @param {User} user The user that reacted - * @returns {?string} Reason to end the collector, if any - * @private + * Handle a reaction deletion for possible disposal. + * @param {MessageReaction} reaction The reaction to possibly dispose + * @returns {?Snowflake|string} The reaction key */ - postCheck(reaction, user) { - this.users.set(user.id, user); - if (this.options.max && ++this.total >= this.options.max) return 'limit'; + dispose(reaction) { + return reaction.message.id === this.message.id && !reaction.count ? ReactionCollector.key(reaction) : null; + } + + /** + * Empty this reaction collector. + */ + empty() { + this.total = 0; + this.collected.clear(); + this.users.clear(); + this.checkEnd(); + } + + endReason() { + if (this.options.max && this.total >= this.options.max) return 'limit'; if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) return 'emojiLimit'; if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; return null; } /** - * Remove event listeners. - * @private + * Get the collector key for a reaction. + * @param {MessageReaction} reaction The message reaction to get the key for + * @returns {Snowflake|string} The emoji ID (if custom) or the emoji name (if native; will be unicode) */ - cleanup() { - this.client.removeListener('messageReactionAdd', this.listener); + static key(reaction) { + return reaction.emoji.id || reaction.emoji.name; } } diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 77fe507c0..790f7e9ec 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -12,6 +12,7 @@ const EventEmitter = require('events'); * Options to be applied to the collector. * @typedef {Object} CollectorOptions * @property {number} [time] How long to run the collector for + * @property {boolean} [dispose=false] Whether to dispose data when it's deleted */ /** @@ -61,23 +62,19 @@ class Collector extends EventEmitter { */ this._timeout = null; - /** - * Call this to handle an event as a collectable element - * Accepts any event data as parameters - * @type {Function} - * @private - */ - this.listener = this._handle.bind(this); + this.handleCollect = this.handleCollect.bind(this); + this.handleDispose = this.handleDispose.bind(this); + if (options.time) this._timeout = this.client.setTimeout(() => this.stop('time'), options.time); } /** + * Call this to handle an event as a collectable element. Accepts any event data as parameters. * @param {...*} args The arguments emitted by the listener * @emits Collector#collect - * @private */ - _handle(...args) { - const collect = this.handle(...args); + handleCollect(...args) { + const collect = this.collect(...args); if (!collect || !this.filter(...args)) return; this.collected.set(collect.key, collect.value); @@ -86,12 +83,34 @@ class Collector extends EventEmitter { * Emitted whenever an element is collected. * @event Collector#collect * @param {*} element The element that got collected - * @param {Collector} collector The collector + * @param {...*} args The arguments emitted by the listener */ - this.emit('collect', collect.value, this); + this.emit('collect', collect.value, ...args); + this.checkEnd(); + } - const post = this.postCheck(...args); - if (post) this.stop(post); + /** + * Call this to remove an element from the collection. Accepts any event data as parameters. + * @param {...*} args The arguments emitted by the listener + * @emits Collector#dispose + */ + handleDispose(...args) { + if (!this.options.dispose) return; + + const dispose = this.dispose(...args); + if (!dispose || !this.filter(...args) || !this.collected.has(dispose)) return; + + const value = this.collected.get(dispose); + this.collected.delete(dispose); + + /** + * Emitted whenever an element has been disposed. + * @event Collector#dispose + * @param {*} element The element that was disposed + * @param {...*} args The arguments emitted by the listener + */ + this.emit('dispose', value, ...args); + this.checkEnd(); } /** @@ -137,7 +156,6 @@ class Collector extends EventEmitter { if (this._timeout) this.client.clearTimeout(this._timeout); this.ended = true; - this.cleanup(); /** * Emitted when the collector is finished collecting. @@ -148,30 +166,41 @@ class Collector extends EventEmitter { this.emit('end', this.collected, reason); } + /** + * Check whether the collector should end, and if so, end it. + */ + checkEnd() { + const reason = this.endReason(); + if (reason) this.stop(reason); + } + /* eslint-disable no-empty-function, valid-jsdoc */ /** - * Handles incoming events from the `listener` function. Returns null if the event should not be collected, - * or returns an object describing the data that should be stored. - * @see Collector#listener + * Handles incoming events from the `handleCollect` function. Returns null if the event should not + * be collected, or returns an object describing the data that should be stored. + * @see Collector#handleCollect * @param {...*} args Any args the event listener emits - * @returns {?{key: string, value}} Data to insert into collection, if any + * @returns {?{key, value}} Data to insert into collection, if any * @abstract */ - handle() {} + collect() {} /** - * This method runs after collection to see if the collector should finish. + * Handles incoming events from the the `handleDispose`. Returns null if the event should not + * be disposed, or returns the key that should be removed. + * @see Collector#handleDispose * @param {...*} args Any args the event listener emits + * @returns {?*} Key to remove from the collection, if any + * @abstract + */ + dispose() {} + + /** + * The reason this collector has ended or will end with. * @returns {?string} Reason to end the collector, if any * @abstract */ - postCheck() {} - - /** - * Called when the collector is ending. - * @abstract - */ - cleanup() {} + endReason() {} /* eslint-enable no-empty-function, valid-jsdoc */ } From 317bf4f7cb94549a1871a38cdf5b9a3f93739b12 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 22 Jul 2017 16:21:50 -0500 Subject: [PATCH 0158/1359] Revert "retry on 500 (#1709)" This reverts commit 57b69803132355b506f9919f37a55915ccdf86d3. --- src/client/rest/handlers/RequestHandler.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js index d89c5b4a0..e5dfcebe9 100644 --- a/src/client/rest/handlers/RequestHandler.js +++ b/src/client/rest/handlers/RequestHandler.js @@ -45,9 +45,6 @@ class RequestHandler { if (err.status === 429) { this.queue.unshift(item); finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset); - } else if (err.status === 500) { - this.queue.unshift(item); - finish(1e3 + this.client.options.restTimeOffset); } else { item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); finish(); From 07178a0a2ad437de492ec4561e7a73a7758bbfc6 Mon Sep 17 00:00:00 2001 From: Crawl Date: Wed, 26 Jul 2017 23:00:46 +0200 Subject: [PATCH 0159/1359] Revert "rewrite ratelimiting and api route builder (#1667)" This reverts commit a2eeafc75de02cf1651a89c91164657ec6e15aca. --- src/client/Client.js | 8 +- src/client/rest/APIRequest.js | 12 ++- src/client/rest/APIRouter.js | 40 ++++---- src/client/rest/RESTManager.js | 23 +++-- src/client/rest/RequestHandlers/Burst.js | 65 ++++++++++++ .../rest/RequestHandlers/RequestHandler.js | 54 ++++++++++ src/client/rest/RequestHandlers/Sequential.js | 98 +++++++++++++++++++ src/client/rest/handlers/RequestHandler.js | 67 ------------- src/client/rest/handlers/burst.js | 13 --- src/client/rest/handlers/index.js | 5 - src/client/rest/handlers/sequential.js | 16 --- src/structures/Channel.js | 2 +- src/structures/ClientUser.js | 6 +- src/structures/Emoji.js | 2 +- src/structures/Guild.js | 40 ++++---- src/structures/GuildChannel.js | 8 +- src/structures/GuildMember.js | 14 +-- src/structures/Message.js | 12 +-- src/structures/User.js | 8 +- src/structures/Webhook.js | 10 +- 20 files changed, 316 insertions(+), 187 deletions(-) create mode 100644 src/client/rest/RequestHandlers/Burst.js create mode 100644 src/client/rest/RequestHandlers/RequestHandler.js create mode 100644 src/client/rest/RequestHandlers/Sequential.js delete mode 100644 src/client/rest/handlers/RequestHandler.js delete mode 100644 src/client/rest/handlers/burst.js delete mode 100644 src/client/rest/handlers/index.js delete mode 100644 src/client/rest/handlers/sequential.js diff --git a/src/client/Client.js b/src/client/Client.js index 69986e071..df12aadfb 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -332,7 +332,7 @@ class Client extends EventEmitter { */ fetchUser(id, cache = true) { if (this.users.has(id)) return Promise.resolve(this.users.get(id)); - return this.api.users(id).get().then(data => + return this.api.users[id].get().then(data => cache ? this.dataManager.newUser(data) : new User(this, data) ); } @@ -344,7 +344,7 @@ class Client extends EventEmitter { */ fetchInvite(invite) { const code = this.resolver.resolveInviteCode(invite); - return this.api.invites(code).get({ query: { with_counts: true } }) + return this.api.invites[code].get({ query: { with_counts: true } }) .then(data => new Invite(this, data)); } @@ -355,7 +355,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchWebhook(id, token) { - return this.api.webhooks(id, token).get().then(data => new Webhook(this, data)); + return this.api.webhooks.opts(id, token).get().then(data => new Webhook(this, data)); } /** @@ -414,7 +414,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchApplication(id = '@me') { - return this.api.oauth2.applications(id).get() + return this.api.oauth2.applications[id].get() .then(app => new OAuth2Application(this, app)); } diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index b8914a6d2..9382c9ae2 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -8,10 +8,20 @@ class APIRequest { this.client = rest.client; this.method = method; this.path = path.toString(); - this.route = options.route; + this.route = this.getRoute(this.path); this.options = options; } + 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() { if (this.client.token && this.client.user && this.client.user.bot) { return `Bot ${this.client.token}`; diff --git a/src/client/rest/APIRouter.js b/src/client/rest/APIRouter.js index 88103305d..e9771e902 100644 --- a/src/client/rest/APIRouter.js +++ b/src/client/rest/APIRouter.js @@ -1,34 +1,32 @@ const util = require('util'); -const noop = () => {}; // eslint-disable-line no-empty-function const methods = ['get', 'post', 'delete', 'patch', 'put']; const reflectors = [ 'toString', 'valueOf', 'inspect', 'constructor', Symbol.toPrimitive, util.inspect.custom, ]; -function buildRoute(manager) { - const route = ['']; +module.exports = restManager => { const handler = { - get(target, name) { - if (reflectors.includes(name)) return () => route.join('/'); - if (methods.includes(name)) { - return options => manager.request(name, route.join('/'), Object.assign({ - route: route.map((r, i) => { - if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id'; - return r; - }).join('/'), - }, options)); + get(list, name) { + if (name === 'opts') { + function toReturn(...args) { // eslint-disable-line no-inner-declarations + list.push(...args.filter(x => x !== null && typeof x !== 'undefined')); + return new Proxy(list, handler); + } + const directJoin = () => `${list.join('/')}/${name}`; + for (const r of reflectors) toReturn[r] = directJoin; + for (const method of methods) { + toReturn[method] = options => restManager.request(method, directJoin(), options); + } + return toReturn; } - route.push(name); - return new Proxy(noop, handler); - }, - apply(target, _, args) { - route.push(...args.filter(x => x != null)); // eslint-disable-line eqeqeq - return new Proxy(noop, handler); + if (reflectors.includes(name)) return () => list.join('/'); + if (methods.includes(name)) return options => restManager.request(name, list.join('/'), options); + list.push(name); + return new Proxy(list, handler); }, }; - return new Proxy(noop, handler); -} -module.exports = buildRoute; + return new Proxy([''], handler); +}; diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 530623245..70c53629f 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -1,7 +1,8 @@ const UserAgentManager = require('./UserAgentManager'); -const handlers = require('./handlers'); +const SequentialRequestHandler = require('./RequestHandlers/Sequential'); +const BurstRequestHandler = require('./RequestHandlers/Burst'); const APIRequest = require('./APIRequest'); -const routeBuilder = require('./APIRouter'); +const mountApi = require('./APIRouter'); const { Error } = require('../../errors'); class RESTManager { @@ -14,7 +15,7 @@ class RESTManager { } get api() { - return routeBuilder(this); + return mountApi(this); } destroy() { @@ -34,17 +35,21 @@ class RESTManager { } getRequestHandler() { - const method = this.client.options.apiRequestMethod; - if (typeof method === 'function') return method; - const handler = handlers[method]; - if (!handler) throw new Error('RATELIMIT_INVALID_METHOD'); - return handler; + switch (this.client.options.apiRequestMethod) { + case 'sequential': + return SequentialRequestHandler; + case 'burst': + return BurstRequestHandler; + default: + throw new Error('RATELIMIT_INVALID_METHOD'); + } } request(method, url, options = {}) { const apiRequest = new APIRequest(this, method, url, options); if (!this.handlers[apiRequest.route]) { - this.handlers[apiRequest.route] = new handlers.RequestHandler(this, this.getRequestHandler()); + const RequestHandlerType = this.getRequestHandler(); + this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route); } return this.push(this.handlers[apiRequest.route], apiRequest); diff --git a/src/client/rest/RequestHandlers/Burst.js b/src/client/rest/RequestHandlers/Burst.js new file mode 100644 index 000000000..f5968857f --- /dev/null +++ b/src/client/rest/RequestHandlers/Burst.js @@ -0,0 +1,65 @@ +const RequestHandler = require('./RequestHandler'); +const DiscordAPIError = require('../DiscordAPIError'); + +class BurstRequestHandler extends RequestHandler { + constructor(restManager, endpoint) { + super(restManager, endpoint); + + this.client = restManager.client; + + this.limit = Infinity; + this.resetTime = null; + this.remaining = 1; + this.timeDifference = 0; + + this.resetTimeout = null; + } + + push(request) { + super.push(request); + this.handle(); + } + + execute(item) { + if (!item) return; + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.limit = Number(res.headers['x-ratelimit-limit']); + this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.remaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + if (this.resetTimeout) return; + this.resetTimeout = this.client.setTimeout(() => { + this.remaining = this.limit; + this.globalLimit = false; + this.handle(); + this.resetTimeout = null; + }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else { + item.reject(err.status === 400 ? new DiscordAPIError(res.request.path, res.body) : err); + this.handle(); + } + } else { + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + this.handle(); + } + }); + } + + handle() { + super.handle(); + if (this.remaining <= 0 || this.queue.length === 0 || this.globalLimit) return; + this.execute(this.queue.shift()); + this.remaining--; + this.handle(); + } +} + +module.exports = BurstRequestHandler; diff --git a/src/client/rest/RequestHandlers/RequestHandler.js b/src/client/rest/RequestHandlers/RequestHandler.js new file mode 100644 index 000000000..c5bad20ab --- /dev/null +++ b/src/client/rest/RequestHandlers/RequestHandler.js @@ -0,0 +1,54 @@ +/** + * A base class for different types of rate limiting handlers for the REST API. + * @private + */ +class RequestHandler { + /** + * @param {RESTManager} restManager The REST manager to use + */ + constructor(restManager) { + /** + * The RESTManager that instantiated this RequestHandler + * @type {RESTManager} + */ + this.restManager = restManager; + + /** + * A list of requests that have yet to be processed + * @type {APIRequest[]} + */ + this.queue = []; + } + + /** + * Whether or not the client is being rate limited on every endpoint + * @type {boolean} + * @readonly + */ + get globalLimit() { + return this.restManager.globallyRateLimited; + } + + set globalLimit(value) { + this.restManager.globallyRateLimited = value; + } + + /** + * Push a new API request into this bucket. + * @param {APIRequest} request The new request to push into the queue + */ + push(request) { + this.queue.push(request); + } + + /** + * Attempts to get this RequestHandler to process its current queue. + */ + handle() {} // eslint-disable-line no-empty-function + + destroy() { + this.queue = []; + } +} + +module.exports = RequestHandler; diff --git a/src/client/rest/RequestHandlers/Sequential.js b/src/client/rest/RequestHandlers/Sequential.js new file mode 100644 index 000000000..107d7a670 --- /dev/null +++ b/src/client/rest/RequestHandlers/Sequential.js @@ -0,0 +1,98 @@ +const RequestHandler = require('./RequestHandler'); +const DiscordAPIError = require('../DiscordAPIError'); + +/** + * Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto + * the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account, + * but it can be slower. + * @extends {RequestHandler} + * @private + */ +class SequentialRequestHandler extends RequestHandler { + /** + * @param {RESTManager} restManager The REST manager to use + * @param {string} endpoint The endpoint to handle + */ + constructor(restManager, endpoint) { + super(restManager, endpoint); + + /** + * The endpoint that this handler is handling + * @type {string} + */ + this.endpoint = endpoint; + + /** + * The time difference between Discord's Dates and the local computer's Dates. A positive number means the local + * computer's time is ahead of Discord's + * @type {number} + */ + this.timeDifference = 0; + + /** + * Whether the queue is being processed or not + * @type {boolean} + */ + this.busy = false; + } + + push(request) { + super.push(request); + this.handle(); + } + + /** + * Performs a request then resolves a promise to indicate its readiness for a new request. + * @param {APIRequest} item The item to execute + * @returns {Promise} + */ + execute(item) { + this.busy = true; + return new Promise(resolve => { + item.request.gen().end((err, res) => { + if (res && res.headers) { + this.requestLimit = Number(res.headers['x-ratelimit-limit']); + this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.requestRemaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + this.restManager.client.setTimeout(() => { + this.globalLimit = false; + resolve(); + }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset); + if (res.headers['x-ratelimit-global']) this.globalLimit = true; + } else { + item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); + resolve(err); + } + } else { + this.globalLimit = false; + const data = res && res.body ? res.body : {}; + item.resolve(data); + if (this.requestRemaining === 0) { + this.restManager.client.setTimeout( + () => resolve(data), + this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset + ); + } else { + resolve(data); + } + } + }); + }); + } + + handle() { + super.handle(); + if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return; + this.execute(this.queue.shift()).then(() => { + this.busy = false; + this.handle(); + }); + } +} + +module.exports = SequentialRequestHandler; diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js deleted file mode 100644 index e5dfcebe9..000000000 --- a/src/client/rest/handlers/RequestHandler.js +++ /dev/null @@ -1,67 +0,0 @@ -const DiscordAPIError = require('../DiscordAPIError'); - -class RequestHandler { - constructor(manager, handler) { - this.manager = manager; - this.client = this.manager.client; - this.handle = handler.bind(this); - this.limit = Infinity; - this.resetTime = null; - this.remaining = 1; - this.timeDifference = 0; - - this.queue = []; - } - - get limited() { - return this.queue.length === 0 || this.manager.globallyRateLimited || this.remaining <= 0; - } - - set globallyLimited(limited) { - this.manager.globallyRateLimited = limited; - } - - push(request) { - this.queue.push(request); - this.handle(); - } - - execute(item) { - return new Promise((resolve, reject) => { - const finish = timeout => { - // eslint-disable-next-line prefer-promise-reject-errors - if (timeout || this.limited) reject({ timeout, limited: this.limited }); - else resolve(); - }; - item.request.gen().end((err, res) => { - if (res && res.headers) { - if (res.headers['x-ratelimit-global']) this.globallyLimited = true; - this.limit = Number(res.headers['x-ratelimit-limit']); - this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; - this.remaining = Number(res.headers['x-ratelimit-remaining']); - this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); - } - if (err) { - if (err.status === 429) { - this.queue.unshift(item); - finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset); - } else { - item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); - finish(); - } - } else { - const data = res && res.body ? res.body : {}; - item.resolve(data); - finish(); - } - }); - }); - } - - reset() { - this.globallyLimited = false; - this.remaining = 1; - } -} - -module.exports = RequestHandler; diff --git a/src/client/rest/handlers/burst.js b/src/client/rest/handlers/burst.js deleted file mode 100644 index b4e4fd2e3..000000000 --- a/src/client/rest/handlers/burst.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = function burst() { - if (this.limited) return; - this.execute(this.queue.shift()) - .then(this.handle.bind(this)) - .catch(({ timeout }) => { - this.client.setTimeout(() => { - this.reset(); - this.handle(); - }, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset)); - }); - this.remaining--; - this.handle(); -}; diff --git a/src/client/rest/handlers/index.js b/src/client/rest/handlers/index.js deleted file mode 100644 index 47792c46c..000000000 --- a/src/client/rest/handlers/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - sequential: require('./sequential'), - burst: require('./burst'), - RequestHandler: require('./RequestHandler'), -}; diff --git a/src/client/rest/handlers/sequential.js b/src/client/rest/handlers/sequential.js deleted file mode 100644 index 1f644dc17..000000000 --- a/src/client/rest/handlers/sequential.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = function sequential() { - if (this.busy || this.limited) return; - this.busy = true; - this.execute(this.queue.shift()) - .then(() => { - this.busy = false; - this.handle(); - }) - .catch(({ timeout }) => { - this.client.setTimeout(() => { - this.reset(); - this.busy = false; - this.handle(); - }, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset)); - }); -}; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index d8f877c9a..463179649 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -62,7 +62,7 @@ class Channel { * .catch(console.error); // Log error */ delete() { - return this.client.api.channels(this.id).delete().then(() => this); + return this.client.api.channels[this.id].delete().then(() => this); } } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index e0a2f3ba2..4d26487ed 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -91,7 +91,7 @@ class ClientUser extends User { if (data.new_password) _data.new_password = data.newPassword; } - return this.client.api.users('@me').patch({ data }) + return this.client.api.users['@me'].patch({ data }) .then(newData => this.client.actions.UserUpdate.handle(newData).updated); } @@ -284,7 +284,7 @@ class ClientUser extends User { if (options.guild instanceof Guild) options.guild = options.guild.id; Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - return this.client.api.users('@me').mentions.get({ query: options }) + return this.client.api.users['@me'].mentions.get({ query: options }) .then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); } @@ -351,7 +351,7 @@ class ClientUser extends User { return o; }, {}), } : { recipients: recipients.map(u => this.client.resolver.resolveUserID(u)) }; - return this.client.api.users('@me').channels.post({ data }) + return this.client.api.users['@me'].channels.post({ data }) .then(res => new GroupDMChannel(this.client, res)); } } diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 3010d578b..fc3ab07e9 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -121,7 +121,7 @@ class Emoji { * .catch(console.error); */ edit(data, reason) { - return this.client.api.guilds(this.guild.id).emojis(this.id) + return this.client.api.guilds[this.guild.id].emojis[this.id] .patch({ data: { name: data.name, roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : [], diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1c82869ba..0666d72f8 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -378,7 +378,7 @@ class Guild { * @returns {Promise>} */ fetchBans() { - return this.client.api.guilds(this.id).bans.get().then(bans => + return this.client.api.guilds[this.id].bans.get().then(bans => bans.reduce((collection, ban) => { collection.set(ban.user.id, { reason: ban.reason, @@ -394,7 +394,7 @@ class Guild { * @returns {Promise>} */ fetchInvites() { - return this.client.api.guilds(this.id).invites.get() + return this.client.api.guilds[this.id].invites.get() .then(inviteItems => { const invites = new Collection(); for (const inviteItem of inviteItems) { @@ -410,7 +410,7 @@ class Guild { * @returns {Promise>} */ fetchWebhooks() { - return this.client.api.guilds(this.id).webhooks.get().then(data => { + return this.client.api.guilds[this.id].webhooks.get().then(data => { const hooks = new Collection(); for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); return hooks; @@ -422,7 +422,7 @@ class Guild { * @returns {Promise>} */ fetchVoiceRegions() { - return this.client.api.guilds(this.id).regions.get().then(res => { + return this.client.api.guilds[this.id].regions.get().then(res => { const regions = new Collection(); for (const region of res) regions.set(region.id, new VoiceRegion(region)); return regions; @@ -444,7 +444,7 @@ class Guild { if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; - return this.client.api.guilds(this.id)['audit-logs'].get({ query: { + return this.client.api.guilds[this.id]['audit-logs'].get({ query: { before: options.before, after: options.after, limit: options.limit, @@ -476,7 +476,7 @@ class Guild { options.roles = roles.map(role => role.id); } } - return this.client.api.guilds(this.id).members(user.id).put({ data: options }) + return this.client.api.guilds[this.id].members[user.id].put({ data: options }) .then(data => this.client.actions.GuildMemberGet.handle(this, data).member); } @@ -490,7 +490,7 @@ class Guild { user = this.client.resolver.resolveUser(user); if (!user) return Promise.reject(new Error('USER_NOT_CACHED')); if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); - return this.client.api.guilds(this.id).members(user.id).get() + return this.client.api.guilds[this.id].members[user.id].get() .then(data => { if (cache) return this.client.actions.GuildMemberGet.handle(this, data).member; else return new GuildMember(this, data); @@ -597,7 +597,7 @@ class Guild { if (typeof data.explicitContentFilter !== 'undefined') { _data.explicit_content_filter = Number(data.explicitContentFilter); } - return this.client.api.guilds(this.id).patch({ data: _data, reason }) + return this.client.api.guilds[this.id].patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } @@ -742,7 +742,7 @@ class Guild { * @returns {Promise} */ acknowledge() { - return this.client.api.guilds(this.id).ack + return this.client.api.guilds[this.id].ack .post({ data: { token: this.client.rest._ackToken } }) .then(res => { if (res.token) this.client.rest._ackToken = res.token; @@ -781,7 +781,7 @@ class Guild { if (options.days) options['delete-message-days'] = options.days; const id = this.client.resolver.resolveUserID(user); if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true)); - return this.client.api.guilds(this.id).bans[id].put({ query: options }) + return this.client.api.guilds[this.id].bans[id].put({ query: options }) .then(() => { if (user instanceof GuildMember) return user; const _user = this.client.resolver.resolveUser(id); @@ -830,7 +830,7 @@ class Guild { */ pruneMembers({ days = 7, dry = false, reason } = {}) { if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); - return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) + return this.client.api.guilds[this.id].prune[dry ? 'get' : 'post']({ query: { days }, reason }) .then(data => data.pruned); } @@ -865,7 +865,7 @@ class Guild { id: overwrite.id, })); } - return this.client.api.guilds(this.id).channels.post({ + return this.client.api.guilds[this.id].channels.post({ data: { name, type, permission_overwrites: overwrites, }, @@ -898,7 +898,7 @@ class Guild { }; } - return this.client.api.guilds(this.id).channels.patch({ data: { + return this.client.api.guilds[this.id].channels.patch({ data: { guild_id: this.id, channels: channelPositions, } }).then(() => @@ -936,7 +936,7 @@ class Guild { if (data.color) data.color = Util.resolveColor(data.color); if (data.permissions) data.permissions = Permissions.resolve(data.permissions); - return this.client.api.guilds(this.id).roles.post({ data, reason }).then(role => + return this.client.api.guilds[this.id].roles.post({ data, reason }).then(role => this.client.actions.GuildRoleCreate.handle({ guild_id: this.id, role, @@ -965,7 +965,7 @@ class Guild { if (typeof attachment === 'string' && attachment.startsWith('data:')) { const data = { image: attachment, name }; if (roles) data.roles = roles.map(r => r.id ? r.id : r); - return this.client.api.guilds(this.id).emojis.post({ data }) + return this.client.api.guilds[this.id].emojis.post({ data }) .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); } else { return this.client.resolver.resolveBuffer(attachment) @@ -983,7 +983,7 @@ class Guild { */ deleteEmoji(emoji) { if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); - return this.client.api.guilds(this.id).emojis(emoji.id).delete() + return this.client.api.guilds(this.id).emojis[emoji.id].delete() .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } @@ -998,7 +998,7 @@ class Guild { */ leave() { if (this.ownerID === this.client.user.id) return Promise.reject(new Error('GUILD_OWNED')); - return this.client.api.users('@me').guilds(this.id).delete() + return this.client.api.users['@me'].guilds[this.id].delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } @@ -1012,7 +1012,7 @@ class Guild { * .catch(console.error); */ delete() { - return this.client.api.guilds(this.id).delete() + return this.client.api.guilds[this.id].delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } @@ -1170,7 +1170,7 @@ class Guild { Util.moveElementInArray(updatedRoles, role, position, relative); updatedRoles = updatedRoles.map((r, i) => ({ id: r.id, position: i })); - return this.client.api.guilds(this.id).roles.patch({ data: updatedRoles }) + return this.client.api.guilds[this.id].roles.patch({ data: updatedRoles }) .then(() => this.client.actions.GuildRolesPositionUpdate.handle({ guild_id: this.id, @@ -1202,7 +1202,7 @@ class Guild { Util.moveElementInArray(updatedChannels, channel, position, relative); updatedChannels = updatedChannels.map((r, i) => ({ id: r.id, position: i })); - return this.client.api.guilds(this.id).channels.patch({ data: updatedChannels }) + return this.client.api.guilds[this.id].channels.patch({ data: updatedChannels }) .then(() => this.client.actions.GuildChannelsPositionUpdate.handle({ guild_id: this.id, diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 7d405c564..ff2e33a58 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -189,7 +189,7 @@ class GuildChannel extends Channel { } } - return this.client.api.channels(this.id).permissions[payload.id] + return this.client.api.channels[this.id].permissions[payload.id] .put({ data: payload, reason }) .then(() => this); } @@ -216,7 +216,7 @@ class GuildChannel extends Channel { * .catch(console.error); */ edit(data, reason) { - return this.client.api.channels(this.id).patch({ + return this.client.api.channels[this.id].patch({ data: { name: (data.name || this.name).trim(), topic: data.topic || this.topic, @@ -289,7 +289,7 @@ class GuildChannel extends Channel { * @returns {Promise} */ createInvite({ temporary = false, maxAge = 86400, maxUses = 0, unique, reason } = {}) { - return this.client.api.channels(this.id).invites.post({ data: { + return this.client.api.channels[this.id].invites.post({ data: { temporary, max_age: maxAge, max_uses: maxUses, unique, }, reason }) .then(invite => new Invite(this.client, invite)); @@ -353,7 +353,7 @@ class GuildChannel extends Channel { * .catch(console.error); // Log error */ delete(reason) { - return this.client.api.channels(this.id).delete({ reason }).then(() => this); + return this.client.api.channels[this.id].delete({ reason }).then(() => this); } /** diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index f6a10cbe9..d8dadd7d2 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -342,13 +342,13 @@ class GuildMember { data.channel = null; } if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); - let endpoint = this.client.api.guilds(this.guild.id); + let endpoint = this.client.api.guilds[this.guild.id]; if (this.user.id === this.client.user.id) { const keys = Object.keys(data); - if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me').nick; - else endpoint = endpoint.members(this.id); + if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members['@me'].nick; + else endpoint = endpoint.members[this.id]; } else { - endpoint = endpoint.members(this.id); + endpoint = endpoint.members[this.id]; } return endpoint.patch({ data, reason }).then(newData => this.guild._updateMember(this, newData).mem); } @@ -402,7 +402,7 @@ class GuildMember { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); if (this._roles.includes(role.id)) return Promise.resolve(this); - return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) + return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] .put({ reason }) .then(() => this); } @@ -433,7 +433,7 @@ class GuildMember { removeRole(role, reason) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); - return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) + return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] .delete({ reason }) .then(() => this); } @@ -492,7 +492,7 @@ class GuildMember { * @returns {Promise} */ kick(reason) { - return this.client.api.guilds(this.guild.id).members(this.user.id).delete({ reason }) + return this.client.api.guilds[this.guild.id].members[this.user.id].delete({ reason }) .then(() => this.client.actions.GuildMemberRemove.handle({ guild_id: this.guild.id, diff --git a/src/structures/Message.js b/src/structures/Message.js index 75ae5d7eb..2c737e5c2 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -413,7 +413,7 @@ class Message { * @returns {Promise} */ pin() { - return this.client.api.channels(this.channel.id).pins(this.id).put() + return this.client.api.channels[this.channel.id].pins[this.id].put() .then(() => this); } @@ -422,7 +422,7 @@ class Message { * @returns {Promise} */ unpin() { - return this.client.api.channels(this.channel.id).pins(this.id).delete() + return this.client.api.channels[this.channel.id].pins[this.id].delete() .then(() => this); } @@ -435,7 +435,7 @@ class Message { emoji = this.client.resolver.resolveEmojiIdentifier(emoji); if (!emoji) throw new TypeError('EMOJI_TYPE'); - return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji, '@me') + return this.client.api.channels[this.channel.id].messages[this.id].reactions[emoji]['@me'] .put() .then(() => this._addReaction(Util.parseEmoji(emoji), this.client.user)); } @@ -445,7 +445,7 @@ class Message { * @returns {Promise} */ clearReactions() { - return this.client.api.channels(this.channel.id).messages(this.id).reactions.delete() + return this.client.api.channels[this.channel.id].messages[this.id].reactions.delete() .then(() => this); } @@ -463,7 +463,7 @@ class Message { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.client.api.channels(this.channel.id).messages(this.id) + return this.client.api.channels[this.channel.id].messages[this.id] .delete({ reason }) .then(() => this.client.actions.MessageDelete.handle({ @@ -506,7 +506,7 @@ class Message { * @returns {Promise} */ acknowledge() { - return this.client.api.channels(this.channel.id).messages(this.id).ack + return this.client.api.channels[this.channel.id].messages[this.id].ack .post({ data: { token: this.client.rest._ackToken } }) .then(res => { if (res.token) this.client.rest._ackToken = res.token; diff --git a/src/structures/User.js b/src/structures/User.js index 022f51f60..e3d2e0b4c 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -202,7 +202,7 @@ class User { */ createDM() { if (this.dmChannel) return Promise.resolve(this.dmChannel); - return this.client.api.users(this.client.user.id).channels.post({ data: { + return this.client.api.users[this.client.user.id].channels.post({ data: { recipient_id: this.id, } }) .then(data => this.client.actions.ChannelCreate.handle(data).channel); @@ -214,7 +214,7 @@ class User { */ deleteDM() { if (!this.dmChannel) return Promise.reject(new Error('USER_NO_DMCHANNEL')); - return this.client.api.channels(this.dmChannel.id).delete() + return this.client.api.channels[this.dmChannel.id].delete() .then(data => this.client.actions.ChannelDelete.handle(data).channel); } @@ -224,7 +224,7 @@ class User { * @returns {Promise} */ fetchProfile() { - return this.client.api.users(this.id).profile.get().then(data => new UserProfile(this, data)); + return this.client.api.users[this.id].profile.get().then(data => new UserProfile(this, data)); } /** @@ -234,7 +234,7 @@ class User { * @returns {Promise} */ setNote(note) { - return this.client.api.users('@me').notes(this.id).put({ data: { note } }) + return this.client.api.users['@me'].notes[this.id].put({ data: { note } }) .then(() => this); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index f1ee54fad..20af8554e 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -153,7 +153,7 @@ class Webhook { file.file = buffer; return file; }) - )).then(files => this.client.api.webhooks(this.id, this.token).post({ + )).then(files => this.client.api.webhooks.opts(this.id, this.token).post({ data: options, query: { wait: true }, files, @@ -161,7 +161,7 @@ class Webhook { })); } - return this.client.api.webhooks(this.id, this.token).post({ + return this.client.api.webhooks.opts(this.id, this.token).post({ data: options, query: { wait: true }, auth: false, @@ -190,7 +190,7 @@ class Webhook { * }).catch(console.error); */ sendSlackMessage(body) { - return this.client.api.webhooks(this.id, this.token).slack.post({ + return this.client.api.webhooks.opts(this.id, this.token).slack.post({ query: { wait: true }, auth: false, data: body, @@ -216,7 +216,7 @@ class Webhook { return this.edit({ name, avatar: dataURI }, reason); }); } - return this.client.api.webhooks(this.id, this.token).patch({ + return this.client.api.webhooks.opts(this.id, this.token).patch({ data: { name, avatar }, reason, }).then(data => { @@ -232,7 +232,7 @@ class Webhook { * @returns {Promise} */ delete(reason) { - return this.client.api.webhooks(this.id, this.token).delete({ reason }); + return this.client.api.webhooks.opts(this.id, this.token).delete({ reason }); } } From 080996b5a9f2693b16aa3439252829eb55baf3e1 Mon Sep 17 00:00:00 2001 From: bdistin Date: Wed, 26 Jul 2017 19:51:58 -0500 Subject: [PATCH 0160/1359] fix sequential and burst ratelimiters from going on timeout because the queue is empty (#1722) * create branch for me to work on * fix sequential and burst ratelimiters from going on timeout because the queue is empty --- src/client/rest/handlers/RequestHandler.js | 2 +- src/client/rest/handlers/burst.js | 2 +- src/client/rest/handlers/sequential.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js index d89c5b4a0..44427c80f 100644 --- a/src/client/rest/handlers/RequestHandler.js +++ b/src/client/rest/handlers/RequestHandler.js @@ -14,7 +14,7 @@ class RequestHandler { } get limited() { - return this.queue.length === 0 || this.manager.globallyRateLimited || this.remaining <= 0; + return this.manager.globallyRateLimited || this.remaining <= 0; } set globallyLimited(limited) { diff --git a/src/client/rest/handlers/burst.js b/src/client/rest/handlers/burst.js index b4e4fd2e3..8184d1037 100644 --- a/src/client/rest/handlers/burst.js +++ b/src/client/rest/handlers/burst.js @@ -1,5 +1,5 @@ module.exports = function burst() { - if (this.limited) return; + if (this.limited || this.queue.length === 0) return; this.execute(this.queue.shift()) .then(this.handle.bind(this)) .catch(({ timeout }) => { diff --git a/src/client/rest/handlers/sequential.js b/src/client/rest/handlers/sequential.js index 1f644dc17..efc10b058 100644 --- a/src/client/rest/handlers/sequential.js +++ b/src/client/rest/handlers/sequential.js @@ -1,5 +1,5 @@ module.exports = function sequential() { - if (this.busy || this.limited) return; + if (this.busy || this.limited || this.queue.length === 0) return; this.busy = true; this.execute(this.queue.shift()) .then(() => { From 4adecf4aef004be3074e127326a7f06b07cbca9f Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 27 Jul 2017 03:04:03 +0200 Subject: [PATCH 0161/1359] rewrite ratelimiting and api route builder (#1667) * rewrite ratelimiting and api route builder * more stuff * let people pass their own handlers * Update burst.js * Update RequestHandler.js * Update burst.js * Update sequential.js * Update RequestHandler.js --- src/client/Client.js | 8 +- src/client/rest/APIRequest.js | 12 +-- src/client/rest/APIRouter.js | 40 ++++---- src/client/rest/RESTManager.js | 23 ++--- src/client/rest/RequestHandlers/Burst.js | 65 ------------ .../rest/RequestHandlers/RequestHandler.js | 54 ---------- src/client/rest/RequestHandlers/Sequential.js | 98 ------------------- src/client/rest/handlers/RequestHandler.js | 67 +++++++++++++ src/client/rest/handlers/burst.js | 13 +++ src/client/rest/handlers/index.js | 5 + src/client/rest/handlers/sequential.js | 16 +++ src/structures/Channel.js | 2 +- src/structures/ClientUser.js | 6 +- src/structures/Emoji.js | 2 +- src/structures/Guild.js | 40 ++++---- src/structures/GuildChannel.js | 8 +- src/structures/GuildMember.js | 14 +-- src/structures/Message.js | 12 +-- src/structures/User.js | 8 +- src/structures/Webhook.js | 10 +- 20 files changed, 187 insertions(+), 316 deletions(-) delete mode 100644 src/client/rest/RequestHandlers/Burst.js delete mode 100644 src/client/rest/RequestHandlers/RequestHandler.js delete mode 100644 src/client/rest/RequestHandlers/Sequential.js create mode 100644 src/client/rest/handlers/RequestHandler.js create mode 100644 src/client/rest/handlers/burst.js create mode 100644 src/client/rest/handlers/index.js create mode 100644 src/client/rest/handlers/sequential.js diff --git a/src/client/Client.js b/src/client/Client.js index df12aadfb..69986e071 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -332,7 +332,7 @@ class Client extends EventEmitter { */ fetchUser(id, cache = true) { if (this.users.has(id)) return Promise.resolve(this.users.get(id)); - return this.api.users[id].get().then(data => + return this.api.users(id).get().then(data => cache ? this.dataManager.newUser(data) : new User(this, data) ); } @@ -344,7 +344,7 @@ class Client extends EventEmitter { */ fetchInvite(invite) { const code = this.resolver.resolveInviteCode(invite); - return this.api.invites[code].get({ query: { with_counts: true } }) + return this.api.invites(code).get({ query: { with_counts: true } }) .then(data => new Invite(this, data)); } @@ -355,7 +355,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchWebhook(id, token) { - return this.api.webhooks.opts(id, token).get().then(data => new Webhook(this, data)); + return this.api.webhooks(id, token).get().then(data => new Webhook(this, data)); } /** @@ -414,7 +414,7 @@ class Client extends EventEmitter { * @returns {Promise} */ fetchApplication(id = '@me') { - return this.api.oauth2.applications[id].get() + return this.api.oauth2.applications(id).get() .then(app => new OAuth2Application(this, app)); } diff --git a/src/client/rest/APIRequest.js b/src/client/rest/APIRequest.js index 9382c9ae2..b8914a6d2 100644 --- a/src/client/rest/APIRequest.js +++ b/src/client/rest/APIRequest.js @@ -8,20 +8,10 @@ class APIRequest { this.client = rest.client; this.method = method; this.path = path.toString(); - this.route = this.getRoute(this.path); + this.route = options.route; this.options = options; } - 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() { if (this.client.token && this.client.user && this.client.user.bot) { return `Bot ${this.client.token}`; diff --git a/src/client/rest/APIRouter.js b/src/client/rest/APIRouter.js index e9771e902..88103305d 100644 --- a/src/client/rest/APIRouter.js +++ b/src/client/rest/APIRouter.js @@ -1,32 +1,34 @@ const util = require('util'); +const noop = () => {}; // eslint-disable-line no-empty-function const methods = ['get', 'post', 'delete', 'patch', 'put']; const reflectors = [ 'toString', 'valueOf', 'inspect', 'constructor', Symbol.toPrimitive, util.inspect.custom, ]; -module.exports = restManager => { +function buildRoute(manager) { + const route = ['']; const handler = { - get(list, name) { - if (name === 'opts') { - function toReturn(...args) { // eslint-disable-line no-inner-declarations - list.push(...args.filter(x => x !== null && typeof x !== 'undefined')); - return new Proxy(list, handler); - } - const directJoin = () => `${list.join('/')}/${name}`; - for (const r of reflectors) toReturn[r] = directJoin; - for (const method of methods) { - toReturn[method] = options => restManager.request(method, directJoin(), options); - } - return toReturn; + get(target, name) { + if (reflectors.includes(name)) return () => route.join('/'); + if (methods.includes(name)) { + return options => manager.request(name, route.join('/'), Object.assign({ + route: route.map((r, i) => { + if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id'; + return r; + }).join('/'), + }, options)); } - if (reflectors.includes(name)) return () => list.join('/'); - if (methods.includes(name)) return options => restManager.request(name, list.join('/'), options); - list.push(name); - return new Proxy(list, handler); + route.push(name); + return new Proxy(noop, handler); + }, + apply(target, _, args) { + route.push(...args.filter(x => x != null)); // eslint-disable-line eqeqeq + return new Proxy(noop, handler); }, }; + return new Proxy(noop, handler); +} - return new Proxy([''], handler); -}; +module.exports = buildRoute; diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 70c53629f..530623245 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -1,8 +1,7 @@ const UserAgentManager = require('./UserAgentManager'); -const SequentialRequestHandler = require('./RequestHandlers/Sequential'); -const BurstRequestHandler = require('./RequestHandlers/Burst'); +const handlers = require('./handlers'); const APIRequest = require('./APIRequest'); -const mountApi = require('./APIRouter'); +const routeBuilder = require('./APIRouter'); const { Error } = require('../../errors'); class RESTManager { @@ -15,7 +14,7 @@ class RESTManager { } get api() { - return mountApi(this); + return routeBuilder(this); } destroy() { @@ -35,21 +34,17 @@ class RESTManager { } getRequestHandler() { - switch (this.client.options.apiRequestMethod) { - case 'sequential': - return SequentialRequestHandler; - case 'burst': - return BurstRequestHandler; - default: - throw new Error('RATELIMIT_INVALID_METHOD'); - } + const method = this.client.options.apiRequestMethod; + if (typeof method === 'function') return method; + const handler = handlers[method]; + if (!handler) throw new Error('RATELIMIT_INVALID_METHOD'); + return handler; } request(method, url, options = {}) { const apiRequest = new APIRequest(this, method, url, options); if (!this.handlers[apiRequest.route]) { - const RequestHandlerType = this.getRequestHandler(); - this.handlers[apiRequest.route] = new RequestHandlerType(this, apiRequest.route); + this.handlers[apiRequest.route] = new handlers.RequestHandler(this, this.getRequestHandler()); } return this.push(this.handlers[apiRequest.route], apiRequest); diff --git a/src/client/rest/RequestHandlers/Burst.js b/src/client/rest/RequestHandlers/Burst.js deleted file mode 100644 index f5968857f..000000000 --- a/src/client/rest/RequestHandlers/Burst.js +++ /dev/null @@ -1,65 +0,0 @@ -const RequestHandler = require('./RequestHandler'); -const DiscordAPIError = require('../DiscordAPIError'); - -class BurstRequestHandler extends RequestHandler { - constructor(restManager, endpoint) { - super(restManager, endpoint); - - this.client = restManager.client; - - this.limit = Infinity; - this.resetTime = null; - this.remaining = 1; - this.timeDifference = 0; - - this.resetTimeout = null; - } - - push(request) { - super.push(request); - this.handle(); - } - - execute(item) { - if (!item) return; - item.request.gen().end((err, res) => { - if (res && res.headers) { - this.limit = Number(res.headers['x-ratelimit-limit']); - this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; - this.remaining = Number(res.headers['x-ratelimit-remaining']); - this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); - } - if (err) { - if (err.status === 429) { - this.queue.unshift(item); - if (res.headers['x-ratelimit-global']) this.globalLimit = true; - if (this.resetTimeout) return; - this.resetTimeout = this.client.setTimeout(() => { - this.remaining = this.limit; - this.globalLimit = false; - this.handle(); - this.resetTimeout = null; - }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); - } else { - item.reject(err.status === 400 ? new DiscordAPIError(res.request.path, res.body) : err); - this.handle(); - } - } else { - this.globalLimit = false; - const data = res && res.body ? res.body : {}; - item.resolve(data); - this.handle(); - } - }); - } - - handle() { - super.handle(); - if (this.remaining <= 0 || this.queue.length === 0 || this.globalLimit) return; - this.execute(this.queue.shift()); - this.remaining--; - this.handle(); - } -} - -module.exports = BurstRequestHandler; diff --git a/src/client/rest/RequestHandlers/RequestHandler.js b/src/client/rest/RequestHandlers/RequestHandler.js deleted file mode 100644 index c5bad20ab..000000000 --- a/src/client/rest/RequestHandlers/RequestHandler.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * A base class for different types of rate limiting handlers for the REST API. - * @private - */ -class RequestHandler { - /** - * @param {RESTManager} restManager The REST manager to use - */ - constructor(restManager) { - /** - * The RESTManager that instantiated this RequestHandler - * @type {RESTManager} - */ - this.restManager = restManager; - - /** - * A list of requests that have yet to be processed - * @type {APIRequest[]} - */ - this.queue = []; - } - - /** - * Whether or not the client is being rate limited on every endpoint - * @type {boolean} - * @readonly - */ - get globalLimit() { - return this.restManager.globallyRateLimited; - } - - set globalLimit(value) { - this.restManager.globallyRateLimited = value; - } - - /** - * Push a new API request into this bucket. - * @param {APIRequest} request The new request to push into the queue - */ - push(request) { - this.queue.push(request); - } - - /** - * Attempts to get this RequestHandler to process its current queue. - */ - handle() {} // eslint-disable-line no-empty-function - - destroy() { - this.queue = []; - } -} - -module.exports = RequestHandler; diff --git a/src/client/rest/RequestHandlers/Sequential.js b/src/client/rest/RequestHandlers/Sequential.js deleted file mode 100644 index 107d7a670..000000000 --- a/src/client/rest/RequestHandlers/Sequential.js +++ /dev/null @@ -1,98 +0,0 @@ -const RequestHandler = require('./RequestHandler'); -const DiscordAPIError = require('../DiscordAPIError'); - -/** - * Handles API Requests sequentially, i.e. we wait until the current request is finished before moving onto - * the next. This plays a _lot_ nicer in terms of avoiding 429's when there is more than one session of the account, - * but it can be slower. - * @extends {RequestHandler} - * @private - */ -class SequentialRequestHandler extends RequestHandler { - /** - * @param {RESTManager} restManager The REST manager to use - * @param {string} endpoint The endpoint to handle - */ - constructor(restManager, endpoint) { - super(restManager, endpoint); - - /** - * The endpoint that this handler is handling - * @type {string} - */ - this.endpoint = endpoint; - - /** - * The time difference between Discord's Dates and the local computer's Dates. A positive number means the local - * computer's time is ahead of Discord's - * @type {number} - */ - this.timeDifference = 0; - - /** - * Whether the queue is being processed or not - * @type {boolean} - */ - this.busy = false; - } - - push(request) { - super.push(request); - this.handle(); - } - - /** - * Performs a request then resolves a promise to indicate its readiness for a new request. - * @param {APIRequest} item The item to execute - * @returns {Promise} - */ - execute(item) { - this.busy = true; - return new Promise(resolve => { - item.request.gen().end((err, res) => { - if (res && res.headers) { - this.requestLimit = Number(res.headers['x-ratelimit-limit']); - this.requestResetTime = Number(res.headers['x-ratelimit-reset']) * 1000; - this.requestRemaining = Number(res.headers['x-ratelimit-remaining']); - this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); - } - if (err) { - if (err.status === 429) { - this.queue.unshift(item); - this.restManager.client.setTimeout(() => { - this.globalLimit = false; - resolve(); - }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset); - if (res.headers['x-ratelimit-global']) this.globalLimit = true; - } else { - item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); - resolve(err); - } - } else { - this.globalLimit = false; - const data = res && res.body ? res.body : {}; - item.resolve(data); - if (this.requestRemaining === 0) { - this.restManager.client.setTimeout( - () => resolve(data), - this.requestResetTime - Date.now() + this.timeDifference + this.restManager.client.options.restTimeOffset - ); - } else { - resolve(data); - } - } - }); - }); - } - - handle() { - super.handle(); - if (this.busy || this.remaining === 0 || this.queue.length === 0 || this.globalLimit) return; - this.execute(this.queue.shift()).then(() => { - this.busy = false; - this.handle(); - }); - } -} - -module.exports = SequentialRequestHandler; diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js new file mode 100644 index 000000000..e5dfcebe9 --- /dev/null +++ b/src/client/rest/handlers/RequestHandler.js @@ -0,0 +1,67 @@ +const DiscordAPIError = require('../DiscordAPIError'); + +class RequestHandler { + constructor(manager, handler) { + this.manager = manager; + this.client = this.manager.client; + this.handle = handler.bind(this); + this.limit = Infinity; + this.resetTime = null; + this.remaining = 1; + this.timeDifference = 0; + + this.queue = []; + } + + get limited() { + return this.queue.length === 0 || this.manager.globallyRateLimited || this.remaining <= 0; + } + + set globallyLimited(limited) { + this.manager.globallyRateLimited = limited; + } + + push(request) { + this.queue.push(request); + this.handle(); + } + + execute(item) { + return new Promise((resolve, reject) => { + const finish = timeout => { + // eslint-disable-next-line prefer-promise-reject-errors + if (timeout || this.limited) reject({ timeout, limited: this.limited }); + else resolve(); + }; + item.request.gen().end((err, res) => { + if (res && res.headers) { + if (res.headers['x-ratelimit-global']) this.globallyLimited = true; + this.limit = Number(res.headers['x-ratelimit-limit']); + this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000; + this.remaining = Number(res.headers['x-ratelimit-remaining']); + this.timeDifference = Date.now() - new Date(res.headers.date).getTime(); + } + if (err) { + if (err.status === 429) { + this.queue.unshift(item); + finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else { + item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); + finish(); + } + } else { + const data = res && res.body ? res.body : {}; + item.resolve(data); + finish(); + } + }); + }); + } + + reset() { + this.globallyLimited = false; + this.remaining = 1; + } +} + +module.exports = RequestHandler; diff --git a/src/client/rest/handlers/burst.js b/src/client/rest/handlers/burst.js new file mode 100644 index 000000000..b4e4fd2e3 --- /dev/null +++ b/src/client/rest/handlers/burst.js @@ -0,0 +1,13 @@ +module.exports = function burst() { + if (this.limited) return; + this.execute(this.queue.shift()) + .then(this.handle.bind(this)) + .catch(({ timeout }) => { + this.client.setTimeout(() => { + this.reset(); + this.handle(); + }, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset)); + }); + this.remaining--; + this.handle(); +}; diff --git a/src/client/rest/handlers/index.js b/src/client/rest/handlers/index.js new file mode 100644 index 000000000..47792c46c --- /dev/null +++ b/src/client/rest/handlers/index.js @@ -0,0 +1,5 @@ +module.exports = { + sequential: require('./sequential'), + burst: require('./burst'), + RequestHandler: require('./RequestHandler'), +}; diff --git a/src/client/rest/handlers/sequential.js b/src/client/rest/handlers/sequential.js new file mode 100644 index 000000000..1f644dc17 --- /dev/null +++ b/src/client/rest/handlers/sequential.js @@ -0,0 +1,16 @@ +module.exports = function sequential() { + if (this.busy || this.limited) return; + this.busy = true; + this.execute(this.queue.shift()) + .then(() => { + this.busy = false; + this.handle(); + }) + .catch(({ timeout }) => { + this.client.setTimeout(() => { + this.reset(); + this.busy = false; + this.handle(); + }, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset)); + }); +}; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 463179649..d8f877c9a 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -62,7 +62,7 @@ class Channel { * .catch(console.error); // Log error */ delete() { - return this.client.api.channels[this.id].delete().then(() => this); + return this.client.api.channels(this.id).delete().then(() => this); } } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 4d26487ed..e0a2f3ba2 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -91,7 +91,7 @@ class ClientUser extends User { if (data.new_password) _data.new_password = data.newPassword; } - return this.client.api.users['@me'].patch({ data }) + return this.client.api.users('@me').patch({ data }) .then(newData => this.client.actions.UserUpdate.handle(newData).updated); } @@ -284,7 +284,7 @@ class ClientUser extends User { if (options.guild instanceof Guild) options.guild = options.guild.id; Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - return this.client.api.users['@me'].mentions.get({ query: options }) + return this.client.api.users('@me').mentions.get({ query: options }) .then(data => data.map(m => new Message(this.client.channels.get(m.channel_id), m, this.client))); } @@ -351,7 +351,7 @@ class ClientUser extends User { return o; }, {}), } : { recipients: recipients.map(u => this.client.resolver.resolveUserID(u)) }; - return this.client.api.users['@me'].channels.post({ data }) + return this.client.api.users('@me').channels.post({ data }) .then(res => new GroupDMChannel(this.client, res)); } } diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index fc3ab07e9..3010d578b 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -121,7 +121,7 @@ class Emoji { * .catch(console.error); */ edit(data, reason) { - return this.client.api.guilds[this.guild.id].emojis[this.id] + return this.client.api.guilds(this.guild.id).emojis(this.id) .patch({ data: { name: data.name, roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : [], diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 0666d72f8..1c82869ba 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -378,7 +378,7 @@ class Guild { * @returns {Promise>} */ fetchBans() { - return this.client.api.guilds[this.id].bans.get().then(bans => + return this.client.api.guilds(this.id).bans.get().then(bans => bans.reduce((collection, ban) => { collection.set(ban.user.id, { reason: ban.reason, @@ -394,7 +394,7 @@ class Guild { * @returns {Promise>} */ fetchInvites() { - return this.client.api.guilds[this.id].invites.get() + return this.client.api.guilds(this.id).invites.get() .then(inviteItems => { const invites = new Collection(); for (const inviteItem of inviteItems) { @@ -410,7 +410,7 @@ class Guild { * @returns {Promise>} */ fetchWebhooks() { - return this.client.api.guilds[this.id].webhooks.get().then(data => { + return this.client.api.guilds(this.id).webhooks.get().then(data => { const hooks = new Collection(); for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook)); return hooks; @@ -422,7 +422,7 @@ class Guild { * @returns {Promise>} */ fetchVoiceRegions() { - return this.client.api.guilds[this.id].regions.get().then(res => { + return this.client.api.guilds(this.id).regions.get().then(res => { const regions = new Collection(); for (const region of res) regions.set(region.id, new VoiceRegion(region)); return regions; @@ -444,7 +444,7 @@ class Guild { if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; - return this.client.api.guilds[this.id]['audit-logs'].get({ query: { + return this.client.api.guilds(this.id)['audit-logs'].get({ query: { before: options.before, after: options.after, limit: options.limit, @@ -476,7 +476,7 @@ class Guild { options.roles = roles.map(role => role.id); } } - return this.client.api.guilds[this.id].members[user.id].put({ data: options }) + return this.client.api.guilds(this.id).members(user.id).put({ data: options }) .then(data => this.client.actions.GuildMemberGet.handle(this, data).member); } @@ -490,7 +490,7 @@ class Guild { user = this.client.resolver.resolveUser(user); if (!user) return Promise.reject(new Error('USER_NOT_CACHED')); if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); - return this.client.api.guilds[this.id].members[user.id].get() + return this.client.api.guilds(this.id).members(user.id).get() .then(data => { if (cache) return this.client.actions.GuildMemberGet.handle(this, data).member; else return new GuildMember(this, data); @@ -597,7 +597,7 @@ class Guild { if (typeof data.explicitContentFilter !== 'undefined') { _data.explicit_content_filter = Number(data.explicitContentFilter); } - return this.client.api.guilds[this.id].patch({ data: _data, reason }) + return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } @@ -742,7 +742,7 @@ class Guild { * @returns {Promise} */ acknowledge() { - return this.client.api.guilds[this.id].ack + return this.client.api.guilds(this.id).ack .post({ data: { token: this.client.rest._ackToken } }) .then(res => { if (res.token) this.client.rest._ackToken = res.token; @@ -781,7 +781,7 @@ class Guild { if (options.days) options['delete-message-days'] = options.days; const id = this.client.resolver.resolveUserID(user); if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true)); - return this.client.api.guilds[this.id].bans[id].put({ query: options }) + return this.client.api.guilds(this.id).bans[id].put({ query: options }) .then(() => { if (user instanceof GuildMember) return user; const _user = this.client.resolver.resolveUser(id); @@ -830,7 +830,7 @@ class Guild { */ pruneMembers({ days = 7, dry = false, reason } = {}) { if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); - return this.client.api.guilds[this.id].prune[dry ? 'get' : 'post']({ query: { days }, reason }) + return this.client.api.guilds(this.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) .then(data => data.pruned); } @@ -865,7 +865,7 @@ class Guild { id: overwrite.id, })); } - return this.client.api.guilds[this.id].channels.post({ + return this.client.api.guilds(this.id).channels.post({ data: { name, type, permission_overwrites: overwrites, }, @@ -898,7 +898,7 @@ class Guild { }; } - return this.client.api.guilds[this.id].channels.patch({ data: { + return this.client.api.guilds(this.id).channels.patch({ data: { guild_id: this.id, channels: channelPositions, } }).then(() => @@ -936,7 +936,7 @@ class Guild { if (data.color) data.color = Util.resolveColor(data.color); if (data.permissions) data.permissions = Permissions.resolve(data.permissions); - return this.client.api.guilds[this.id].roles.post({ data, reason }).then(role => + return this.client.api.guilds(this.id).roles.post({ data, reason }).then(role => this.client.actions.GuildRoleCreate.handle({ guild_id: this.id, role, @@ -965,7 +965,7 @@ class Guild { if (typeof attachment === 'string' && attachment.startsWith('data:')) { const data = { image: attachment, name }; if (roles) data.roles = roles.map(r => r.id ? r.id : r); - return this.client.api.guilds[this.id].emojis.post({ data }) + return this.client.api.guilds(this.id).emojis.post({ data }) .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); } else { return this.client.resolver.resolveBuffer(attachment) @@ -983,7 +983,7 @@ class Guild { */ deleteEmoji(emoji) { if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); - return this.client.api.guilds(this.id).emojis[emoji.id].delete() + return this.client.api.guilds(this.id).emojis(emoji.id).delete() .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } @@ -998,7 +998,7 @@ class Guild { */ leave() { if (this.ownerID === this.client.user.id) return Promise.reject(new Error('GUILD_OWNED')); - return this.client.api.users['@me'].guilds[this.id].delete() + return this.client.api.users('@me').guilds(this.id).delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } @@ -1012,7 +1012,7 @@ class Guild { * .catch(console.error); */ delete() { - return this.client.api.guilds[this.id].delete() + return this.client.api.guilds(this.id).delete() .then(() => this.client.actions.GuildDelete.handle({ id: this.id }).guild); } @@ -1170,7 +1170,7 @@ class Guild { Util.moveElementInArray(updatedRoles, role, position, relative); updatedRoles = updatedRoles.map((r, i) => ({ id: r.id, position: i })); - return this.client.api.guilds[this.id].roles.patch({ data: updatedRoles }) + return this.client.api.guilds(this.id).roles.patch({ data: updatedRoles }) .then(() => this.client.actions.GuildRolesPositionUpdate.handle({ guild_id: this.id, @@ -1202,7 +1202,7 @@ class Guild { Util.moveElementInArray(updatedChannels, channel, position, relative); updatedChannels = updatedChannels.map((r, i) => ({ id: r.id, position: i })); - return this.client.api.guilds[this.id].channels.patch({ data: updatedChannels }) + return this.client.api.guilds(this.id).channels.patch({ data: updatedChannels }) .then(() => this.client.actions.GuildChannelsPositionUpdate.handle({ guild_id: this.id, diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index ff2e33a58..7d405c564 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -189,7 +189,7 @@ class GuildChannel extends Channel { } } - return this.client.api.channels[this.id].permissions[payload.id] + return this.client.api.channels(this.id).permissions[payload.id] .put({ data: payload, reason }) .then(() => this); } @@ -216,7 +216,7 @@ class GuildChannel extends Channel { * .catch(console.error); */ edit(data, reason) { - return this.client.api.channels[this.id].patch({ + return this.client.api.channels(this.id).patch({ data: { name: (data.name || this.name).trim(), topic: data.topic || this.topic, @@ -289,7 +289,7 @@ class GuildChannel extends Channel { * @returns {Promise} */ createInvite({ temporary = false, maxAge = 86400, maxUses = 0, unique, reason } = {}) { - return this.client.api.channels[this.id].invites.post({ data: { + return this.client.api.channels(this.id).invites.post({ data: { temporary, max_age: maxAge, max_uses: maxUses, unique, }, reason }) .then(invite => new Invite(this.client, invite)); @@ -353,7 +353,7 @@ class GuildChannel extends Channel { * .catch(console.error); // Log error */ delete(reason) { - return this.client.api.channels[this.id].delete({ reason }).then(() => this); + return this.client.api.channels(this.id).delete({ reason }).then(() => this); } /** diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d8dadd7d2..f6a10cbe9 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -342,13 +342,13 @@ class GuildMember { data.channel = null; } if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); - let endpoint = this.client.api.guilds[this.guild.id]; + let endpoint = this.client.api.guilds(this.guild.id); if (this.user.id === this.client.user.id) { const keys = Object.keys(data); - if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members['@me'].nick; - else endpoint = endpoint.members[this.id]; + if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me').nick; + else endpoint = endpoint.members(this.id); } else { - endpoint = endpoint.members[this.id]; + endpoint = endpoint.members(this.id); } return endpoint.patch({ data, reason }).then(newData => this.guild._updateMember(this, newData).mem); } @@ -402,7 +402,7 @@ class GuildMember { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); if (this._roles.includes(role.id)) return Promise.resolve(this); - return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) .put({ reason }) .then(() => this); } @@ -433,7 +433,7 @@ class GuildMember { removeRole(role, reason) { if (!(role instanceof Role)) role = this.guild.roles.get(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); - return this.client.api.guilds[this.guild.id].members[this.user.id].roles[role.id] + return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) .delete({ reason }) .then(() => this); } @@ -492,7 +492,7 @@ class GuildMember { * @returns {Promise} */ kick(reason) { - return this.client.api.guilds[this.guild.id].members[this.user.id].delete({ reason }) + return this.client.api.guilds(this.guild.id).members(this.user.id).delete({ reason }) .then(() => this.client.actions.GuildMemberRemove.handle({ guild_id: this.guild.id, diff --git a/src/structures/Message.js b/src/structures/Message.js index 2c737e5c2..75ae5d7eb 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -413,7 +413,7 @@ class Message { * @returns {Promise} */ pin() { - return this.client.api.channels[this.channel.id].pins[this.id].put() + return this.client.api.channels(this.channel.id).pins(this.id).put() .then(() => this); } @@ -422,7 +422,7 @@ class Message { * @returns {Promise} */ unpin() { - return this.client.api.channels[this.channel.id].pins[this.id].delete() + return this.client.api.channels(this.channel.id).pins(this.id).delete() .then(() => this); } @@ -435,7 +435,7 @@ class Message { emoji = this.client.resolver.resolveEmojiIdentifier(emoji); if (!emoji) throw new TypeError('EMOJI_TYPE'); - return this.client.api.channels[this.channel.id].messages[this.id].reactions[emoji]['@me'] + return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji, '@me') .put() .then(() => this._addReaction(Util.parseEmoji(emoji), this.client.user)); } @@ -445,7 +445,7 @@ class Message { * @returns {Promise} */ clearReactions() { - return this.client.api.channels[this.channel.id].messages[this.id].reactions.delete() + return this.client.api.channels(this.channel.id).messages(this.id).reactions.delete() .then(() => this); } @@ -463,7 +463,7 @@ class Message { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.client.api.channels[this.channel.id].messages[this.id] + return this.client.api.channels(this.channel.id).messages(this.id) .delete({ reason }) .then(() => this.client.actions.MessageDelete.handle({ @@ -506,7 +506,7 @@ class Message { * @returns {Promise} */ acknowledge() { - return this.client.api.channels[this.channel.id].messages[this.id].ack + return this.client.api.channels(this.channel.id).messages(this.id).ack .post({ data: { token: this.client.rest._ackToken } }) .then(res => { if (res.token) this.client.rest._ackToken = res.token; diff --git a/src/structures/User.js b/src/structures/User.js index e3d2e0b4c..022f51f60 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -202,7 +202,7 @@ class User { */ createDM() { if (this.dmChannel) return Promise.resolve(this.dmChannel); - return this.client.api.users[this.client.user.id].channels.post({ data: { + return this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }) .then(data => this.client.actions.ChannelCreate.handle(data).channel); @@ -214,7 +214,7 @@ class User { */ deleteDM() { if (!this.dmChannel) return Promise.reject(new Error('USER_NO_DMCHANNEL')); - return this.client.api.channels[this.dmChannel.id].delete() + return this.client.api.channels(this.dmChannel.id).delete() .then(data => this.client.actions.ChannelDelete.handle(data).channel); } @@ -224,7 +224,7 @@ class User { * @returns {Promise} */ fetchProfile() { - return this.client.api.users[this.id].profile.get().then(data => new UserProfile(this, data)); + return this.client.api.users(this.id).profile.get().then(data => new UserProfile(this, data)); } /** @@ -234,7 +234,7 @@ class User { * @returns {Promise} */ setNote(note) { - return this.client.api.users['@me'].notes[this.id].put({ data: { note } }) + return this.client.api.users('@me').notes(this.id).put({ data: { note } }) .then(() => this); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 20af8554e..f1ee54fad 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -153,7 +153,7 @@ class Webhook { file.file = buffer; return file; }) - )).then(files => this.client.api.webhooks.opts(this.id, this.token).post({ + )).then(files => this.client.api.webhooks(this.id, this.token).post({ data: options, query: { wait: true }, files, @@ -161,7 +161,7 @@ class Webhook { })); } - return this.client.api.webhooks.opts(this.id, this.token).post({ + return this.client.api.webhooks(this.id, this.token).post({ data: options, query: { wait: true }, auth: false, @@ -190,7 +190,7 @@ class Webhook { * }).catch(console.error); */ sendSlackMessage(body) { - return this.client.api.webhooks.opts(this.id, this.token).slack.post({ + return this.client.api.webhooks(this.id, this.token).slack.post({ query: { wait: true }, auth: false, data: body, @@ -216,7 +216,7 @@ class Webhook { return this.edit({ name, avatar: dataURI }, reason); }); } - return this.client.api.webhooks.opts(this.id, this.token).patch({ + return this.client.api.webhooks(this.id, this.token).patch({ data: { name, avatar }, reason, }).then(data => { @@ -232,7 +232,7 @@ class Webhook { * @returns {Promise} */ delete(reason) { - return this.client.api.webhooks.opts(this.id, this.token).delete({ reason }); + return this.client.api.webhooks(this.id, this.token).delete({ reason }); } } From 91c9df2da31b841637343adb2adf31fc1571965d Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 22 Jul 2017 16:21:50 -0500 Subject: [PATCH 0162/1359] Revert "Revert "retry on 500 (#1709)"" This reverts commit 317bf4f7cb94549a1871a38cdf5b9a3f93739b12. --- src/client/rest/handlers/RequestHandler.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js index e5dfcebe9..d89c5b4a0 100644 --- a/src/client/rest/handlers/RequestHandler.js +++ b/src/client/rest/handlers/RequestHandler.js @@ -45,6 +45,9 @@ class RequestHandler { if (err.status === 429) { this.queue.unshift(item); finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else if (err.status === 500) { + this.queue.unshift(item); + finish(1e3 + this.client.options.restTimeOffset); } else { item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err); finish(); From b2ab94718079fe93bd171f906d43cf3ba182ad34 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 26 Jul 2017 20:12:41 -0500 Subject: [PATCH 0163/1359] fix guild audit log fetching of stuff (#1721) * Update GuildAuditLogs.js * Update GuildAuditLogs.js * e --- src/structures/GuildAuditLogs.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index b1f1e9db8..64ec9a611 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -241,18 +241,29 @@ class GuildAuditLogsEntry { * The target of this entry * @type {Snowflake|Guild|User|Role|Emoji|Invite|Webhook} */ - this.target = data.target_id; + this.target = this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, {}); + this.target.id = data.target_id; } else if ([Targets.USER, Targets.GUILD].includes(targetType)) { this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = this.webhooks.get(data.target_id); } else if (targetType === Targets.INVITE) { - const change = this.changes.find(c => c.key === 'code'); - this.target = guild.fetchInvites() - .then(invites => { - this.target = invites.find(i => i.code === (change.new_value || change.old_value)); - return this.target; - }); + if (guild.me.permissions.has('MANAGE_GUILD')) { + const change = this.changes.find(c => c.key === 'code'); + this.target = guild.fetchInvites() + .then(invites => { + this.target = invites.find(i => i.code === (change.new || change.old)); + return this.target; + }); + } else { + this.target = this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, {}); + } } else if (targetType === Targets.MESSAGE) { this.target = guild.client.users.get(data.target_id); } else { From 27196c2f9fc6ec46a569f6375710509b53c6df24 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 27 Jul 2017 03:13:35 +0200 Subject: [PATCH 0164/1359] docs update to topics/voice playFile example (#1716) * Update voice.md * Update voice.md * fixed my grammar * added requested spaces --- docs/topics/voice.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index dc70eb0e9..c7e88d04b 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -46,11 +46,17 @@ have obtained a voice connection, we can start streaming audio to it. The follow file: **Playing a file:** + ```js -// To play a file, we need to give an absolute path to it +// Use an absolute path const dispatcher = connection.playFile('C:/Users/Discord/Desktop/myfile.mp3'); ``` +```js +// Or an dynamic path +const dispatcher = connection.playFile('./myfile.mp3'); +``` + Your file doesn't have to be just an mp3; ffmpeg can convert videos and audios of many formats. The `dispatcher` variable is an instance of a `StreamDispatcher`, which manages streaming a specific resource to a voice @@ -84,8 +90,7 @@ this can also be used: ```js connection.playStream(myReadableStream); -// If you don't want to use absolute paths, you can use -// fs.createReadStream to circumvent it +// You can use fs.createReadStream to create an ReadableStream const fs = require('fs'); const stream = fs.createReadStream('./test.mp3'); From eef87e5d97b02be63916005171a116a7ee2d2f50 Mon Sep 17 00:00:00 2001 From: Pg Biel Date: Wed, 26 Jul 2017 22:14:04 -0300 Subject: [PATCH 0165/1359] Reasons (#1715) * Add reasons * How could I forget * Hopefully fix conflicts --- src/structures/Emoji.js | 5 +-- src/structures/Guild.js | 58 ++++++++++++++++++++-------------- src/structures/GuildChannel.js | 25 +++++++++------ src/structures/Role.js | 25 +++++++++------ src/structures/TextChannel.js | 5 +-- src/structures/VoiceChannel.js | 10 +++--- 6 files changed, 78 insertions(+), 50 deletions(-) diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 3010d578b..8391ad37b 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -132,10 +132,11 @@ class Emoji { /** * Set the name of the emoji. * @param {string} name The new name for the emoji + * @param {string} [reason] Reason for changing the emoji's name * @returns {Promise} */ - setName(name) { - return this.edit({ name }); + setName(name, reason) { + return this.edit({ name }, reason); } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1c82869ba..1fe93e93c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -604,15 +604,17 @@ class Guild { /** * Edit the level of the explicit content filter. * @param {number} explicitContentFilter The new level of the explicit content filter + * @param {string} [reason] Reason for changing the level of the guild's explicit content filter * @returns {Promise} */ - setExplicitContentFilter(explicitContentFilter) { - return this.edit({ explicitContentFilter }); + setExplicitContentFilter(explicitContentFilter, reason) { + return this.edit({ explicitContentFilter }, reason); } /** * Edit the name of the guild. * @param {string} name The new name of the guild + * @param {string} [reason] Reason for changing the guild's name * @returns {Promise} * @example * // Edit the guild name @@ -620,13 +622,14 @@ class Guild { * .then(updated => console.log(`Updated guild name to ${guild.name}`)) * .catch(console.error); */ - setName(name) { - return this.edit({ name }); + setName(name, reason) { + return this.edit({ name }, reason); } /** * Edit the region of the guild. * @param {string} region The new region of the guild + * @param {string} [reason] Reason for changing the guild's region * @returns {Promise} * @example * // Edit the guild region @@ -634,13 +637,14 @@ class Guild { * .then(updated => console.log(`Updated guild region to ${guild.region}`)) * .catch(console.error); */ - setRegion(region) { - return this.edit({ region }); + setRegion(region, reason) { + return this.edit({ region }, reason); } /** * Edit the verification level of the guild. * @param {number} verificationLevel The new verification level of the guild + * @param {string} [reason] Reason for changing the guild's verification level * @returns {Promise} * @example * // Edit the guild verification level @@ -648,13 +652,14 @@ class Guild { * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`)) * .catch(console.error); */ - setVerificationLevel(verificationLevel) { - return this.edit({ verificationLevel }); + setVerificationLevel(verificationLevel, reason) { + return this.edit({ verificationLevel }, reason); } /** * Edit the AFK channel of the guild. * @param {ChannelResolvable} afkChannel The new AFK channel + * @param {string} [reason] Reason for changing the guild's AFK channel * @returns {Promise} * @example * // Edit the guild AFK channel @@ -662,13 +667,14 @@ class Guild { * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel}`)) * .catch(console.error); */ - setAFKChannel(afkChannel) { - return this.edit({ afkChannel }); + setAFKChannel(afkChannel, reason) { + return this.edit({ afkChannel }, reason); } /** * Edit the AFK timeout of the guild. * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK + * @param {string} [reason] Reason for changing the guild's AFK timeout * @returns {Promise} * @example * // Edit the guild AFK channel @@ -676,13 +682,14 @@ class Guild { * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`)) * .catch(console.error); */ - setAFKTimeout(afkTimeout) { - return this.edit({ afkTimeout }); + setAFKTimeout(afkTimeout, reason) { + return this.edit({ afkTimeout }, reason); } /** * Set a new guild icon. * @param {Base64Resolvable} icon The new icon of the guild + * @param {string} [reason] Reason for changing the guild's icon * @returns {Promise} * @example * // Edit the guild icon @@ -690,13 +697,14 @@ class Guild { * .then(updated => console.log('Updated the guild icon')) * .catch(console.error); */ - setIcon(icon) { - return this.edit({ icon }); + setIcon(icon, reason) { + return this.edit({ icon }, reason); } /** * Sets a new owner of the guild. * @param {GuildMemberResolvable} owner The new owner of the guild + * @param {string} [reason] Reason for setting the new owner * @returns {Promise} * @example * // Edit the guild owner @@ -704,13 +712,14 @@ class Guild { * .then(updated => console.log(`Updated the guild owner to ${updated.owner.username}`)) * .catch(console.error); */ - setOwner(owner) { - return this.edit({ owner }); + setOwner(owner, reason) { + return this.edit({ owner }, reason); } /** * Set a new guild splash screen. * @param {Base64Resolvable} splash The new splash screen of the guild + * @param {string} [reason] Reason for changing the guild's splash screen * @returns {Promise} * @example * // Edit the guild splash @@ -718,8 +727,8 @@ class Guild { * .then(updated => console.log('Updated the guild splash')) * .catch(console.error); */ - setSplash(splash) { - return this.edit({ splash }); + setSplash(splash, reason) { + return this.edit({ splash }, reason); } /** @@ -948,7 +957,9 @@ class Guild { * Creates a new custom emoji in the guild. * @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji * @param {string} name The name for the emoji - * @param {Collection|Role[]} [roles] Roles to limit the emoji to + * @param {Object} [options] Options + * @param {Collection|Role[]} [options.roles] Roles to limit the emoji to + * @param {string} [options.reason] Reason for creating the emoji * @returns {Promise} The created emoji * @example * // Create a new emoji from a url @@ -961,11 +972,11 @@ class Guild { * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) * .catch(console.error); */ - createEmoji(attachment, name, roles) { + createEmoji(attachment, name, { roles, reason } = {}) { if (typeof attachment === 'string' && attachment.startsWith('data:')) { const data = { image: attachment, name }; if (roles) data.roles = roles.map(r => r.id ? r.id : r); - return this.client.api.guilds(this.id).emojis.post({ data }) + return this.client.api.guilds(this.id).emojis.post({ data, reason }) .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); } else { return this.client.resolver.resolveBuffer(attachment) @@ -979,11 +990,12 @@ class Guild { /** * Delete an emoji. * @param {Emoji|string} emoji The emoji to delete + * @param {string} [reason] Reason for deleting the emoji * @returns {Promise} */ - deleteEmoji(emoji) { + deleteEmoji(emoji, reason) { if (!(emoji instanceof Emoji)) emoji = this.emojis.get(emoji); - return this.client.api.guilds(this.id).emojis(emoji.id).delete() + return this.client.api.guilds(this.id).emojis(emoji.id).delete({ reason }) .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 7d405c564..76dec642a 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -231,6 +231,7 @@ class GuildChannel extends Channel { /** * Set a new name for the guild channel. * @param {string} name The new name for the guild channel + * @param {string} [reason] Reason for changing the guild channel's name * @returns {Promise} * @example * // Set a new channel name @@ -238,8 +239,8 @@ class GuildChannel extends Channel { * .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`)) * .catch(console.error); */ - setName(name) { - return this.edit({ name }); + setName(name, reason) { + return this.edit({ name }, reason); } /** @@ -260,6 +261,7 @@ class GuildChannel extends Channel { /** * Set a new topic for the guild channel. * @param {string} topic The new topic for the guild channel + * @param {string} [reason] Reason for changing the guild channel's topic * @returns {Promise} * @example * // Set a new channel topic @@ -267,8 +269,8 @@ class GuildChannel extends Channel { * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) * .catch(console.error); */ - setTopic(topic) { - return this.edit({ topic }); + setTopic(topic, reason) { + return this.edit({ topic }, reason); } /** @@ -297,13 +299,18 @@ class GuildChannel extends Channel { /** * Clone this channel. - * @param {string} [name=this.name] Optional name for the new channel, otherwise it has the name of this channel - * @param {boolean} [withPermissions=true] Whether to clone the channel with this channel's permission overwrites - * @param {boolean} [withTopic=true] Whether to clone the channel with this channel's topic + * @param {Object} [options] The options + * @param {string} [options.name=this.name] Optional name for the new channel, otherwise it has the name + * of this channel + * @param {boolean} [options.withPermissions=true] Whether to clone the channel with this channel's + * permission overwrites + * @param {boolean} [options.withTopic=true] Whether to clone the channel with this channel's topic + * @param {string} [options.reason] Reason for cloning this channel * @returns {Promise} */ - clone(name = this.name, withPermissions = true, withTopic = true) { - return this.guild.createChannel(name, this.type, { overwrites: withPermissions ? this.permissionOverwrites : [] }) + clone({ name = this.name, withPermissions = true, withTopic = true, reason } = {}) { + const options = { overwrites: withPermissions ? this.permissionOverwrites : [], reason }; + return this.guild.createChannel(name, this.type, options) .then(channel => withTopic ? channel.setTopic(this.topic) : channel); } diff --git a/src/structures/Role.js b/src/structures/Role.js index 4ba915674..c9ab6075e 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -218,6 +218,7 @@ class Role { /** * Set a new name for the role. * @param {string} name The new name of the role + * @param {string} [reason] Reason for changing the role's name * @returns {Promise} * @example * // Set the name of the role @@ -225,13 +226,14 @@ class Role { * .then(r => console.log(`Edited name of role ${r}`)) * .catch(console.error); */ - setName(name) { - return this.edit({ name }); + setName(name, reason) { + return this.edit({ name }, reason); } /** * Set a new color for the role. * @param {ColorResolvable} color The color of the role + * @param {string} [reason] Reason for changing the role's color * @returns {Promise} * @example * // Set the color of a role @@ -239,13 +241,14 @@ class Role { * .then(r => console.log(`Set color of role ${r}`)) * .catch(console.error); */ - setColor(color) { - return this.edit({ color }); + setColor(color, reason) { + return this.edit({ color }, reason); } /** * Set whether or not the role should be hoisted. * @param {boolean} hoist Whether or not to hoist the role + * @param {string} [reason] Reason for setting whether or not the role should be hoisted * @returns {Promise} * @example * // Set the hoist of the role @@ -253,8 +256,8 @@ class Role { * .then(r => console.log(`Role hoisted: ${r.hoist}`)) * .catch(console.error); */ - setHoist(hoist) { - return this.edit({ hoist }); + setHoist(hoist, reason) { + return this.edit({ hoist }, reason); } /** @@ -275,6 +278,7 @@ class Role { /** * Set the permissions of the role. * @param {string[]} permissions The permissions of the role + * @param {string} [reason] Reason for changing the role's permissions * @returns {Promise} * @example * // Set the permissions of the role @@ -282,13 +286,14 @@ class Role { * .then(r => console.log(`Role updated ${r}`)) * .catch(console.error); */ - setPermissions(permissions) { - return this.edit({ permissions }); + setPermissions(permissions, reason) { + return this.edit({ permissions }, reason); } /** * Set whether this role is mentionable. * @param {boolean} mentionable Whether this role should be mentionable + * @param {string} [reason] Reason for setting whether or not this role should be mentionable * @returns {Promise} * @example * // Make the role mentionable @@ -296,8 +301,8 @@ class Role { * .then(r => console.log(`Role updated ${r}`)) * .catch(console.error); */ - setMentionable(mentionable) { - return this.edit({ mentionable }); + setMentionable(mentionable, reason) { + return this.edit({ mentionable }, reason); } /** diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index cf65bc9e1..6a401604c 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -66,17 +66,18 @@ class TextChannel extends GuildChannel { * Create a webhook for the channel. * @param {string} name The name of the webhook * @param {BufferResolvable|Base64Resolvable} avatar The avatar for the webhook + * @param {string} [reason] Reason for creating this webhook * @returns {Promise} webhook The created webhook * @example * channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png') * .then(webhook => console.log(`Created webhook ${webhook}`)) * .catch(console.error) */ - createWebhook(name, avatar) { + createWebhook(name, avatar, reason) { if (typeof avatar === 'string' && avatar.startsWith('data:')) { return this.client.api.channels[this.id].webhooks.post({ data: { name, avatar, - } }).then(data => new Webhook(this.client, data)); + }, reason }).then(data => new Webhook(this.client, data)); } else { return this.client.resolver.resolveBuffer(avatar).then(data => this.createWebhook(name, this.client.resolver.resolveBase64(data) || null)); diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 0afa6e4c2..f446d945e 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -79,6 +79,7 @@ class VoiceChannel extends GuildChannel { /** * Sets the bitrate of the channel (in kbps). * @param {number} bitrate The new bitrate + * @param {string} [reason] Reason for changing the channel's bitrate * @returns {Promise} * @example * // Set the bitrate of a voice channel @@ -86,14 +87,15 @@ class VoiceChannel extends GuildChannel { * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`)) * .catch(console.error); */ - setBitrate(bitrate) { + setBitrate(bitrate, reason) { bitrate *= 1000; - return this.edit({ bitrate }); + return this.edit({ bitrate }, reason); } /** * Sets the user limit of the channel. * @param {number} userLimit The new user limit + * @param {string} [reason] Reason for changing the user limit * @returns {Promise} * @example * // Set the user limit of a voice channel @@ -101,8 +103,8 @@ class VoiceChannel extends GuildChannel { * .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`)) * .catch(console.error); */ - setUserLimit(userLimit) { - return this.edit({ userLimit }); + setUserLimit(userLimit, reason) { + return this.edit({ userLimit }, reason); } /** From 7b74b088ac63f049fa2d465c5bd5859cf4e15cce Mon Sep 17 00:00:00 2001 From: Crawl Date: Fri, 28 Jul 2017 04:53:23 +0200 Subject: [PATCH 0166/1359] Update nsfw prop --- src/structures/TextChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 6a401604c..3df3b67db 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -30,7 +30,7 @@ class TextChannel extends GuildChannel { * @type {boolean} * @readonly */ - this.nsfw = data.nsfw; + this.nsfw = !!data.nsfw; this.lastMessageID = data.last_message_id; } From a46f606170755b6bf024256625544040ced6921c Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Thu, 27 Jul 2017 23:17:39 -0400 Subject: [PATCH 0167/1359] Fix Crawl/Drah garbage --- src/structures/TextChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 3df3b67db..279a16b5c 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -30,7 +30,7 @@ class TextChannel extends GuildChannel { * @type {boolean} * @readonly */ - this.nsfw = !!data.nsfw; + this.nsfw = Boolean(data.nsfw); this.lastMessageID = data.last_message_id; } From 0acb0ac5dca66bafe9fe2dc9d3befcbefd1e3167 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 29 Jul 2017 15:02:56 -0500 Subject: [PATCH 0168/1359] fix unpack weird issues (#1729) --- src/client/websocket/WebSocketConnection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index a8a6cb16f..ab6e354c6 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -166,6 +166,7 @@ class WebSocketConnection extends EventEmitter { * @returns {Object} */ unpack(data) { + if (Array.isArray(data)) data = Buffer.concat(data); if (data instanceof ArrayBuffer) data = Buffer.from(new Uint8Array(data)); if (erlpack && typeof data !== 'string') return erlpack.unpack(data); From fe8f371d822ceb1882bae49181e631a0d8dfee3c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 31 Jul 2017 01:16:50 +0200 Subject: [PATCH 0169/1359] Fixed Client#guildMembersChunk's members collection's key being undefined. (#1736) --- src/client/websocket/packets/handlers/GuildMembersChunk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js index 685de80c1..51951b28c 100644 --- a/src/client/websocket/packets/handlers/GuildMembersChunk.js +++ b/src/client/websocket/packets/handlers/GuildMembersChunk.js @@ -10,7 +10,7 @@ class GuildMembersChunkHandler extends AbstractHandler { if (!guild) return; const members = new Collection(); - for (const member of data.members) members.set(member.id, guild._addMember(member, false)); + for (const member of data.members) members.set(member.user.id, guild._addMember(member, false)); client.emit(Constants.Events.GUILD_MEMBERS_CHUNK, members, guild); From 79fed1295c32c7c04c140504bebcb2bd948c3733 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 31 Jul 2017 01:18:55 +0200 Subject: [PATCH 0170/1359] Fixed DiscordAPIError#message sometimes being undefined (#1735) --- src/client/rest/DiscordAPIError.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client/rest/DiscordAPIError.js b/src/client/rest/DiscordAPIError.js index f0a641285..580723bf2 100644 --- a/src/client/rest/DiscordAPIError.js +++ b/src/client/rest/DiscordAPIError.js @@ -5,9 +5,9 @@ class DiscordAPIError extends Error { constructor(path, error) { super(); - const flattened = error.errors ? `\n${this.constructor.flattenErrors(error.errors).join('\n')}` : ''; + const flattened = this.constructor.flattenErrors(error.errors || error).join('\n'); this.name = 'DiscordAPIError'; - this.message = `${error.message}${flattened}`; + this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened; /** * The path of the request relative to the HTTP endpoint @@ -33,12 +33,15 @@ class DiscordAPIError extends Error { let messages = []; for (const [k, v] of Object.entries(obj)) { + if (k === 'message') continue; const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k; if (v._errors) { messages.push(`${newKey}: ${v._errors.map(e => e.message).join(' ')}`); } else if (v.code || v.message) { messages.push(`${v.code ? `${v.code}: ` : ''}${v.message}`.trim()); + } else if (typeof v === 'string') { + messages.push(v); } else { messages = messages.concat(this.flattenErrors(v, newKey)); } From 4f5d1bffaf4aeccee665d7b5bd3701468109fab1 Mon Sep 17 00:00:00 2001 From: tjpc3 Date: Mon, 31 Jul 2017 22:01:45 -0400 Subject: [PATCH 0171/1359] fix node.js crash due to error messages being called with invalid error keys (#1728) * Comments * More Comments * Added new error message * Fixed error message * Removing temporary comments * Fixed error messages * Added more new error messages * Removed trailing space * Removed other trailing space * Changed error key * Changed error key used --- src/client/voice/VoiceWebSocket.js | 6 +++--- src/errors/Messages.js | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceWebSocket.js b/src/client/voice/VoiceWebSocket.js index 4184a22ad..c5927d57a 100644 --- a/src/client/voice/VoiceWebSocket.js +++ b/src/client/voice/VoiceWebSocket.js @@ -65,7 +65,7 @@ class VoiceWebSocket extends EventEmitter { if (this.dead) return; if (this.ws) this.reset(); if (this.attempts >= 5) { - this.emit('debug', new Error(`Too many connection attempts (${this.attempts}).`)); + this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts)); return; } @@ -123,7 +123,7 @@ class VoiceWebSocket extends EventEmitter { session_id: this.voiceConnection.authentication.sessionID, }, }).catch(() => { - this.emit('error', new Error('Tried to send join packet, but the WebSocket is not open.')); + this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED')); }); } @@ -204,7 +204,7 @@ class VoiceWebSocket extends EventEmitter { */ setHeartbeat(interval) { if (!interval || isNaN(interval)) { - this.onError(new Error('Tried to set voice heartbeat but no valid interval was specified.')); + this.onError(new Error('VOICE_INVALID_HEARTBEAT')); return; } if (this.heartbeatInterval) { diff --git a/src/errors/Messages.js b/src/errors/Messages.js index eda968dcb..43f47f370 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -53,6 +53,8 @@ const Messages = { VOICE_SESSION_ABSENT: 'Session ID not supplied.', VOICE_INVALID_ENDPOINT: 'Invalid endpoint received.', VOICE_NO_BROWSER: 'Voice connections are not available in browsers.', + VOICE_CONNECTION_ATTEMPTS_EXCEEDED: attempts => `Too many connection attempts (${attempts}).`, + VOICE_JOIN_SOCKET_CLOSED: 'Tried to send join packet, but the WebSocket is not open.', OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', From b8034525e3da2a5bd05fab44841ee3b0149ff70c Mon Sep 17 00:00:00 2001 From: stupid cat Date: Mon, 31 Jul 2017 20:28:15 -0600 Subject: [PATCH 0172/1359] Add user_guild_settings support (#1365) * user guild settings * Use direct collection * I'm a goof * double goof * Structure properties * Forgot to register listener * wrong class names * No more get in docs * avoid waterfalls, bot checks * trycatch wow i thought i already did this :notlikecat: * :eyes: * Update ClientUser.js * Update ClientUserGuildSettings.js * Update UserGuildSettingsUpdate.js * Update ClientUserChannelOverride.js * Update ClientUserGuildSettings.js --- .../packets/WebSocketPacketManager.js | 1 + .../websocket/packets/handlers/Ready.js | 1 + .../handlers/UserGuildSettingsUpdate.js | 18 ++++++ src/structures/ClientUser.js | 14 +++++ src/structures/ClientUserChannelOverride.js | 29 +++++++++ src/structures/ClientUserGuildSettings.js | 47 ++++++++++++++ src/structures/Guild.js | 56 +++++++++++++++++ src/structures/GuildChannel.js | 30 +++++++++ src/util/Constants.js | 63 +++++++++++++++++++ 9 files changed, 259 insertions(+) create mode 100644 src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js create mode 100644 src/structures/ClientUserChannelOverride.js create mode 100644 src/structures/ClientUserGuildSettings.js diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js index 079ef5d8c..efc42df4a 100644 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ b/src/client/websocket/packets/WebSocketPacketManager.js @@ -39,6 +39,7 @@ class WebSocketPacketManager { this.register(Constants.WSEvents.USER_UPDATE, require('./handlers/UserUpdate')); this.register(Constants.WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate')); this.register(Constants.WSEvents.USER_SETTINGS_UPDATE, require('./handlers/UserSettingsUpdate')); + this.register(Constants.WSEvents.USER_GUILD_SETTINGS_UPDATE, require('./handlers/UserGuildSettingsUpdate')); this.register(Constants.WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate')); this.register(Constants.WSEvents.TYPING_START, require('./handlers/TypingStart')); this.register(Constants.WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate')); diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index d7ee8ed11..8c2492abf 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -10,6 +10,7 @@ class ReadyHandler extends AbstractHandler { client.ws.heartbeat(); data.user.user_settings = data.user_settings; + data.user.user_guild_settings = data.user_guild_settings; const clientUser = new ClientUser(client, data.user); client.user = clientUser; diff --git a/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js b/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js new file mode 100644 index 000000000..db38a724b --- /dev/null +++ b/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js @@ -0,0 +1,18 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class UserGuildSettingsUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + client.user.guildSettings.get(packet.d.guild_id).patch(packet.d); + client.emit(Constants.Events.USER_GUILD_SETTINGS_UPDATE, client.user.guildSettings.get(packet.d.guild_id)); + } +} + +/** + * Emitted whenever the client user's settings update. + * @event Client#clientUserGuildSettingsUpdate + * @param {ClientUserGuildSettings} clientUserGuildSettings The new client user guild settings + */ + +module.exports = UserGuildSettingsUpdateHandler; diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index e0a2f3ba2..476a6f5b0 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,6 +1,7 @@ const User = require('./User'); const Collection = require('../util/Collection'); const ClientUserSettings = require('./ClientUserSettings'); +const ClientUserGuildSettings = require('./ClientUserGuildSettings'); const Constants = require('../util/Constants'); const Util = require('../util/Util'); const Guild = require('./Guild'); @@ -78,6 +79,19 @@ class ClientUser extends User { * @type {?ClientUserSettings} */ if (data.user_settings) this.settings = new ClientUserSettings(this, data.user_settings); + + /** + * All of the user's guild settings + * @type {Collection} + * This is only filled when using a user account + */ + this.guildSettings = new Collection(); + if (data.user_guild_settings) { + for (const settings of data.user_guild_settings) { + const guild = this.client.guilds.get(settings.guild_id); + this.guildSettings.set(guild.id, new ClientUserGuildSettings(settings, guild)); + } + } } edit(data, password) { diff --git a/src/structures/ClientUserChannelOverride.js b/src/structures/ClientUserChannelOverride.js new file mode 100644 index 000000000..3ea594874 --- /dev/null +++ b/src/structures/ClientUserChannelOverride.js @@ -0,0 +1,29 @@ +const Constants = require('../util/Constants'); + +/** + * A wrapper around the ClientUser's channel overrides. + */ +class ClientUserChannelOverride { + constructor(user, data) { + this.user = user; + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + */ + patch(data) { + for (const key of Object.keys(Constants.UserChannelOverrideMap)) { + const value = Constants.UserChannelOverrideMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } +} + +module.exports = ClientUserChannelOverride; diff --git a/src/structures/ClientUserGuildSettings.js b/src/structures/ClientUserGuildSettings.js new file mode 100644 index 000000000..960906379 --- /dev/null +++ b/src/structures/ClientUserGuildSettings.js @@ -0,0 +1,47 @@ +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const ClientUserChannelOverride = require('./ClientUserChannelOverride'); + +/** + * A wrapper around the ClientUser's guild settings. + */ +class ClientUserGuildSettings { + constructor(data, guild) { + this.guild = guild; + this.channelOverrides = new Collection(); + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + */ + patch(data) { + for (const key of Object.keys(Constants.UserGuildSettingsMap)) { + const value = Constants.UserGuildSettingsMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (key === 'channel_overrides') { + for (const channel of data[key]) { + this.channelOverrides.set(channel.channel_id, + new ClientUserChannelOverride(this.guild.client.user, channel)); + } + } else if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } + + /** + * Update a specific property of the guild settings. + * @param {string} name Name of property + * @param {value} value Value to patch + * @returns {Promise} + */ + update(name, value) { + return this.guild.client.api.guilds(this.guild.id).settings.patch({ data: { [name]: value } }); + } +} + +module.exports = ClientUserGuildSettings; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1fe93e93c..db8795591 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -333,6 +333,62 @@ class Guild { } /** + * Whether the guild is muted + * This is only available when using a user account. + * @type {?boolean} + */ + get muted() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).muted; + } catch (err) { + return false; + } + } + + /** + * The type of message that should notify you + * one of `EVERYTHING`, `MENTIONS`, `NOTHING` + * This is only available when using a user account. + * @type {string} + */ + get messageNotifications() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).messageNotifications; + } catch (err) { + return null; + } + } + + /** + * Whether to receive mobile push notifications + * This is only available when using a user account. + * @type {boolean} + */ + get mobilePush() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.id).mobilePush; + } catch (err) { + return false; + } + } + + /** + * Whether to suppress everyone messages + * This is only available when using a user account. + * @type {boolean} + */ + get suppressEveryone() { + try { + return this.client.user.guildSettings.get(this.id).suppressEveryone; + } catch (err) { + return null; + } + } + + /* * The `@everyone` role of the guild * @type {Role} * @readonly diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 76dec642a..20b18376a 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -4,6 +4,7 @@ const Invite = require('./Invite'); const PermissionOverwrites = require('./PermissionOverwrites'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); const { TypeError } = require('../errors'); /** @@ -363,6 +364,35 @@ class GuildChannel extends Channel { return this.client.api.channels(this.id).delete({ reason }).then(() => this); } + /** + * Whether the channel is muted + * This is only available when using a user account. + * @type {boolean} + */ + get muted() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).muted; + } catch (err) { + return false; + } + } + + /** + * The type of message that should notify you + * one of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` + * This is only available when using a user account. + * @type {string} + */ + get messageNotifications() { + if (this.client.user.bot) return null; + try { + return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).messageNotifications; + } catch (err) { + return Constants.MessageNotificationTypes[3]; + } + } + /** * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. * @returns {string} diff --git a/src/util/Constants.js b/src/util/Constants.js index 7af384ba9..cbf6c6a73 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -231,6 +231,7 @@ exports.Events = { USER_UPDATE: 'userUpdate', USER_NOTE_UPDATE: 'userNoteUpdate', USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', + USER_GUILD_SETTINGS_UPDATE: 'clientUserGuildSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', TYPING_START: 'typingStart', @@ -312,6 +313,7 @@ exports.WSEvents = { USER_UPDATE: 'USER_UPDATE', USER_NOTE_UPDATE: 'USER_NOTE_UPDATE', USER_SETTINGS_UPDATE: 'USER_SETTINGS_UPDATE', + USER_GUILD_SETTINGS_UPDATE: 'USER_GUILD_SETTINGS_UPDATE', PRESENCE_UPDATE: 'PRESENCE_UPDATE', VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE', TYPING_START: 'TYPING_START', @@ -349,6 +351,13 @@ exports.ExplicitContentFilterTypes = [ 'FRIENDS_AND_NON_FRIENDS', ]; +exports.MessageNotificationTypes = [ + 'EVERYTHING', + 'MENTIONS', + 'NOTHING', + 'INHERIT', +]; + exports.UserSettingsMap = { /** * Automatically convert emoticons in your messages to emoji @@ -482,6 +491,60 @@ exports.UserSettingsMap = { }, }; +exports.UserGuildSettingsMap = { + message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching + /** + * The type of message that should notify you + * one of `EVERYTHING`, `MENTIONS`, `NOTHING` + * @name ClientUserGuildSettings#messageNotifications + * @type {string} + */ + return exports.MessageNotificationTypes[type]; + }, + /** + * Whether to receive mobile push notifications + * @name ClientUserGuildSettings#mobilePush + * @type {boolean} + */ + mobile_push: 'mobilePush', + /** + * Whether the guild is muted or not + * @name ClientUserGuildSettings#muted + * @type {boolean} + */ + muted: 'muted', + /** + * Whether to suppress everyone messages + * @name ClientUserGuildSettings#suppressEveryone + * @type {boolean} + */ + suppress_everyone: 'suppressEveryone', + /** + * A collection containing all the channel overrides + * @name ClientUserGuildSettings#channelOverrides + * @type {Collection} + */ + channel_overrides: 'channelOverrides', +}; + +exports.UserChannelOverrideMap = { + message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching + /** + * The type of message that should notify you + * one of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` + * @name ClientUserChannelOverrides#messageNotifications + * @type {string} + */ + return exports.MessageNotificationTypes[type]; + }, + /** + * Whether the guild is muted or not + * @name ClientUserChannelOverrides#muted + * @type {boolean} + */ + muted: 'muted', +}; + /** * All flags users can have: * - STAFF From d4793bae0f4c183be2672179b08b45757fffb616 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Tue, 1 Aug 2017 04:32:24 +0200 Subject: [PATCH 0173/1359] Fix tiny doc string --- src/client/websocket/packets/handlers/UserSettingsUpdate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/packets/handlers/UserSettingsUpdate.js b/src/client/websocket/packets/handlers/UserSettingsUpdate.js index 903b64b51..b90d4f589 100644 --- a/src/client/websocket/packets/handlers/UserSettingsUpdate.js +++ b/src/client/websocket/packets/handlers/UserSettingsUpdate.js @@ -10,7 +10,7 @@ class UserSettingsUpdateHandler extends AbstractHandler { } /** - * Emitted when the client user's settings update. + * Emitted whenever the client user's settings update. * @event Client#clientUserSettingsUpdate * @param {ClientUserSettings} clientUserSettings The new client user settings */ From bda72c60fa8e2008535c85fe42b3ce06f9c91dc1 Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 1 Aug 2017 04:40:39 +0200 Subject: [PATCH 0174/1359] CRLF to LF --- .../handlers/UserGuildSettingsUpdate.js | 36 +++---- src/structures/ClientUserChannelOverride.js | 58 ++++++------ src/structures/ClientUserGuildSettings.js | 94 +++++++++---------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js b/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js index db38a724b..1470a3c84 100644 --- a/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js +++ b/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js @@ -1,18 +1,18 @@ -const AbstractHandler = require('./AbstractHandler'); -const Constants = require('../../../../util/Constants'); - -class UserGuildSettingsUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - client.user.guildSettings.get(packet.d.guild_id).patch(packet.d); - client.emit(Constants.Events.USER_GUILD_SETTINGS_UPDATE, client.user.guildSettings.get(packet.d.guild_id)); - } -} - -/** - * Emitted whenever the client user's settings update. - * @event Client#clientUserGuildSettingsUpdate - * @param {ClientUserGuildSettings} clientUserGuildSettings The new client user guild settings - */ - -module.exports = UserGuildSettingsUpdateHandler; +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class UserGuildSettingsUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + client.user.guildSettings.get(packet.d.guild_id).patch(packet.d); + client.emit(Constants.Events.USER_GUILD_SETTINGS_UPDATE, client.user.guildSettings.get(packet.d.guild_id)); + } +} + +/** + * Emitted whenever the client user's settings update. + * @event Client#clientUserGuildSettingsUpdate + * @param {ClientUserGuildSettings} clientUserGuildSettings The new client user guild settings + */ + +module.exports = UserGuildSettingsUpdateHandler; diff --git a/src/structures/ClientUserChannelOverride.js b/src/structures/ClientUserChannelOverride.js index 3ea594874..72414fee5 100644 --- a/src/structures/ClientUserChannelOverride.js +++ b/src/structures/ClientUserChannelOverride.js @@ -1,29 +1,29 @@ -const Constants = require('../util/Constants'); - -/** - * A wrapper around the ClientUser's channel overrides. - */ -class ClientUserChannelOverride { - constructor(user, data) { - this.user = user; - this.patch(data); - } - - /** - * Patch the data contained in this class with new partial data. - * @param {Object} data Data to patch this with - */ - patch(data) { - for (const key of Object.keys(Constants.UserChannelOverrideMap)) { - const value = Constants.UserChannelOverrideMap[key]; - if (!data.hasOwnProperty(key)) continue; - if (typeof value === 'function') { - this[value.name] = value(data[key]); - } else { - this[value] = data[key]; - } - } - } -} - -module.exports = ClientUserChannelOverride; +const Constants = require('../util/Constants'); + +/** + * A wrapper around the ClientUser's channel overrides. + */ +class ClientUserChannelOverride { + constructor(user, data) { + this.user = user; + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + */ + patch(data) { + for (const key of Object.keys(Constants.UserChannelOverrideMap)) { + const value = Constants.UserChannelOverrideMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } +} + +module.exports = ClientUserChannelOverride; diff --git a/src/structures/ClientUserGuildSettings.js b/src/structures/ClientUserGuildSettings.js index 960906379..7bc336558 100644 --- a/src/structures/ClientUserGuildSettings.js +++ b/src/structures/ClientUserGuildSettings.js @@ -1,47 +1,47 @@ -const Constants = require('../util/Constants'); -const Collection = require('../util/Collection'); -const ClientUserChannelOverride = require('./ClientUserChannelOverride'); - -/** - * A wrapper around the ClientUser's guild settings. - */ -class ClientUserGuildSettings { - constructor(data, guild) { - this.guild = guild; - this.channelOverrides = new Collection(); - this.patch(data); - } - - /** - * Patch the data contained in this class with new partial data. - * @param {Object} data Data to patch this with - */ - patch(data) { - for (const key of Object.keys(Constants.UserGuildSettingsMap)) { - const value = Constants.UserGuildSettingsMap[key]; - if (!data.hasOwnProperty(key)) continue; - if (key === 'channel_overrides') { - for (const channel of data[key]) { - this.channelOverrides.set(channel.channel_id, - new ClientUserChannelOverride(this.guild.client.user, channel)); - } - } else if (typeof value === 'function') { - this[value.name] = value(data[key]); - } else { - this[value] = data[key]; - } - } - } - - /** - * Update a specific property of the guild settings. - * @param {string} name Name of property - * @param {value} value Value to patch - * @returns {Promise} - */ - update(name, value) { - return this.guild.client.api.guilds(this.guild.id).settings.patch({ data: { [name]: value } }); - } -} - -module.exports = ClientUserGuildSettings; +const Constants = require('../util/Constants'); +const Collection = require('../util/Collection'); +const ClientUserChannelOverride = require('./ClientUserChannelOverride'); + +/** + * A wrapper around the ClientUser's guild settings. + */ +class ClientUserGuildSettings { + constructor(data, guild) { + this.guild = guild; + this.channelOverrides = new Collection(); + this.patch(data); + } + + /** + * Patch the data contained in this class with new partial data. + * @param {Object} data Data to patch this with + */ + patch(data) { + for (const key of Object.keys(Constants.UserGuildSettingsMap)) { + const value = Constants.UserGuildSettingsMap[key]; + if (!data.hasOwnProperty(key)) continue; + if (key === 'channel_overrides') { + for (const channel of data[key]) { + this.channelOverrides.set(channel.channel_id, + new ClientUserChannelOverride(this.guild.client.user, channel)); + } + } else if (typeof value === 'function') { + this[value.name] = value(data[key]); + } else { + this[value] = data[key]; + } + } + } + + /** + * Update a specific property of the guild settings. + * @param {string} name Name of property + * @param {value} value Value to patch + * @returns {Promise} + */ + update(name, value) { + return this.guild.client.api.guilds(this.guild.id).settings.patch({ data: { [name]: value } }); + } +} + +module.exports = ClientUserGuildSettings; From 73761245cc9cf02e63ec012f0746d96b549e284b Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 1 Aug 2017 04:41:26 +0200 Subject: [PATCH 0175/1359] Update typings --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 99e19b9d7..8e56ab9ac 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 99e19b9d76dde5f1498b58bb9261042d9afa055c +Subproject commit 8e56ab9ac1284fdf9b78b153c78e62d175f99b4b From 5799ba28f910771e704f183e94d273ed7a82a584 Mon Sep 17 00:00:00 2001 From: Pg Biel Date: Mon, 31 Jul 2017 23:48:46 -0300 Subject: [PATCH 0176/1359] Fix Guild.equals (#1713) --- src/structures/Guild.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index db8795591..12a0c6fe8 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1094,23 +1094,24 @@ class Guild { equals(guild) { let equal = guild && + guild instanceof this.constructor && this.id === guild.id && - this.available === !guild.unavailable && + this.available === guild.available && this.splash === guild.splash && this.region === guild.region && this.name === guild.name && - this.memberCount === guild.member_count && + this.memberCount === guild.memberCount && this.large === guild.large && this.icon === guild.icon && Util.arraysEqual(this.features, guild.features) && - this.ownerID === guild.owner_id && - this.verificationLevel === guild.verification_level && - this.embedEnabled === guild.embed_enabled; + this.ownerID === guild.ownerID && + this.verificationLevel === guild.verificationLevel && + this.embedEnabled === guild.embedEnabled; if (equal) { if (this.embedChannel) { - if (this.embedChannel.id !== guild.embed_channel_id) equal = false; - } else if (guild.embed_channel_id) { + if (!guild.embedChannel || this.embedChannel.id !== guild.embedChannel.id) equal = false; + } else if (guild.embedChannel) { equal = false; } } From 963cf42e0dca888034e0b6e5fd99dbab698b0ccf Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 31 Jul 2017 21:49:00 -0500 Subject: [PATCH 0177/1359] add MessageMentions#has, remove old method (#1724) * Update MessageMentions.js * remove old method * smh * Update Message.js * Update MessageMentions.js * Update MessageMentions.js * Update MessageMentions.js --- src/structures/Message.js | 26 -------------------------- src/structures/MessageMentions.js | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 75ae5d7eb..52de85ddd 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -330,32 +330,6 @@ class Message { this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES); } - /** - * Whether or not a user, channel or role is mentioned in this message. - * @param {GuildChannel|User|Role|string} data Either a guild channel, user or a role object, or a string representing - * the ID of any of these - * @returns {boolean} - */ - isMentioned(data) { - data = data && data.id ? data.id : data; - return this.mentions.users.has(data) || this.mentions.channels.has(data) || this.mentions.roles.has(data); - } - - /** - * Whether or not a guild member is mentioned in this message. Takes into account - * user mentions, role mentions, and @everyone/@here mentions. - * @param {GuildMember|User} member The member/user to check for a mention of - * @returns {boolean} - */ - isMemberMentioned(member) { - // Lazy-loading is used here to get around a circular dependency that breaks things - if (!GuildMember) GuildMember = require('./GuildMember'); - if (this.mentions.everyone) return true; - if (this.mentions.users.has(member.id)) return true; - if (member instanceof GuildMember && member.roles.some(r => this.mentions.roles.has(r.id))) return true; - return false; - } - /** * Options that can be passed into editMessage. * @typedef {Object} MessageEditOptions diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 0e8f6de83..85fc764cb 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -1,4 +1,5 @@ const Collection = require('../util/Collection'); +const GuildMember = require('./GuildMember'); /** * Keeps track of mentions in a {@link Message}. @@ -115,6 +116,22 @@ class MessageMentions { } return this._channels; } + + /** + * Check if a user 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 + * @returns {boolean} + */ + has(data, strict = true) { + if (strict && this.everyone) return true; + if (strict && data instanceof GuildMember) { + for (const role of this.roles) 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); + } } /** From 4b77f268511b300e33f1afd838262eb0c9ee5b80 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 31 Jul 2017 22:35:59 -0500 Subject: [PATCH 0178/1359] Update ClientUser.js (#1739) --- src/structures/ClientUser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 476a6f5b0..8852fc439 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -89,7 +89,7 @@ class ClientUser extends User { if (data.user_guild_settings) { for (const settings of data.user_guild_settings) { const guild = this.client.guilds.get(settings.guild_id); - this.guildSettings.set(guild.id, new ClientUserGuildSettings(settings, guild)); + this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(settings, guild)); } } } From fd11381cc5cf534fbe042268a392561f635df195 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 31 Jul 2017 22:42:22 -0500 Subject: [PATCH 0179/1359] fix more race conditions (#1740) * Update ClientUser.js * Update ClientUserGuildSettings.js --- src/structures/ClientUser.js | 1 + src/structures/ClientUserGuildSettings.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 8852fc439..aa7c8edb0 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -88,6 +88,7 @@ class ClientUser extends User { this.guildSettings = new Collection(); if (data.user_guild_settings) { for (const settings of data.user_guild_settings) { + settings.client = this.client; const guild = this.client.guilds.get(settings.guild_id); this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(settings, guild)); } diff --git a/src/structures/ClientUserGuildSettings.js b/src/structures/ClientUserGuildSettings.js index 7bc336558..75d2f8332 100644 --- a/src/structures/ClientUserGuildSettings.js +++ b/src/structures/ClientUserGuildSettings.js @@ -8,6 +8,7 @@ const ClientUserChannelOverride = require('./ClientUserChannelOverride'); class ClientUserGuildSettings { constructor(data, guild) { this.guild = guild; + this.client = data.client; this.channelOverrides = new Collection(); this.patch(data); } @@ -23,7 +24,7 @@ class ClientUserGuildSettings { if (key === 'channel_overrides') { for (const channel of data[key]) { this.channelOverrides.set(channel.channel_id, - new ClientUserChannelOverride(this.guild.client.user, channel)); + new ClientUserChannelOverride(this.client.user, channel)); } } else if (typeof value === 'function') { this[value.name] = value(data[key]); From a30fc878161c497a8f71db2ee62370b59388a297 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Fri, 4 Aug 2017 00:17:10 -0700 Subject: [PATCH 0180/1359] fix reply splitting (#1750) --- src/structures/shared/SendMessage.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index 3eacb1b57..48212295a 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -15,6 +15,14 @@ module.exports = function sendMessage(channel, options) { // eslint-disable-line 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.resolver.resolveUserID(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 = {}; @@ -36,14 +44,6 @@ module.exports = function sendMessage(channel, options) { // eslint-disable-line if (split) content = Util.splitMessage(content, split); } - // Add the reply prefix - if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { - const id = channel.client.resolver.resolveUserID(reply); - const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; - if (split) split.prepend = `${mention}, ${split.prepend || ''}`; - content = `${mention}${typeof content !== 'undefined' ? `, ${content}` : ''}`; - } - if (content instanceof Array) { return new Promise((resolve, reject) => { const messages = []; From abfda7c3cc68dd436953abd438fd42ab59a76209 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 4 Aug 2017 09:17:46 +0200 Subject: [PATCH 0181/1359] Fixed ClientUser#createGroupDM on user accounts and added some more GroupDMChannel stuff (#1747) * ClientUser#createGroupDM now works like the docs states on user accounts * Added GroupDMChannel#setIcon and fixed null handling for the channel name * Added an s * Don't resolve when icon is falsy and removed useless name trimming * Removed now unnecessary name constant * vscode being great * Added GroupDMChannel#iconURL --- src/structures/ClientUser.js | 3 +-- src/structures/GroupDMChannel.js | 32 +++++++++++++++++++++++++++++++- src/util/Constants.js | 4 ++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index aa7c8edb0..8f75a5522 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -345,7 +345,6 @@ class ClientUser extends User { * An object containing either a user or access token, and an optional nickname. * @typedef {Object} GroupDMRecipientOptions * @property {UserResolvable} [user] User to add to the Group DM - * (only available if a user is creating the DM) * @property {string} [accessToken] Access token to use to add a user to the Group DM * (only available if a bot is creating the DM) * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) @@ -365,7 +364,7 @@ class ClientUser extends User { if (r.nick) o[r.user ? r.user.id : r.id] = r.nick; return o; }, {}), - } : { recipients: recipients.map(u => this.client.resolver.resolveUserID(u)) }; + } : { recipients: recipients.map(u => this.client.resolver.resolveUserID(u.user || u.id)) }; return this.client.api.users('@me').channels.post({ data }) .then(res => new GroupDMChannel(this.client, res)); } diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 8c16257b4..9060fa932 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -1,6 +1,7 @@ const Channel = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); /* { type: 3, @@ -103,6 +104,18 @@ class GroupDMChannel extends Channel { return this.client.users.get(this.ownerID); } + /** + * Gets the URL to this Group DM's icon + * @param {Object} [options={}] Options for the icon url + * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` + * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` + * @returns {?string} + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return Constants.Endpoints.CDN(this.client.options.http.cdn).GDMIcon(this.id, this.icon, format, size); + } + /** * Whether this channel equals another channel. It compares all properties, so for most operations * it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often @@ -133,12 +146,29 @@ class GroupDMChannel extends Channel { edit(data, reason) { return this.client.api.channels[this.id].patch({ data: { - name: (data.name || this.name).trim(), + icon: data.icon, + name: data.name === null ? null : data.name || this.name, }, reason, }).then(() => this); } + /** + * Sets a new icon for this Group DM. + * @param {Base64Resolvable} icon The new icon of this Group DM + * @returns {Promise} + */ + setIcon(icon) { + if (typeof icon === 'string' && icon.startsWith('data:')) { + return this.edit({ icon }); + } else if (!icon) { + return this.edit({ icon: null }); + } else { + return this.client.resolver.resolveBuffer(icon) + .then(data => this.edit({ icon: this.client.resolver.resolveBase64(data) })); + } + } + /** * Sets a new name for this Group DM. * @param {string} name New name for this Group DM diff --git a/src/util/Constants.js b/src/util/Constants.js index cbf6c6a73..9c11f9e85 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -119,6 +119,10 @@ exports.Endpoints = { checkImage({ size, format }); return `${root}/app-icons/${clientID}/${hash}.${format}${size ? `?size=${size}` : ''}`; }, + GDMIcon: (channelID, hash, format = 'webp', size) => { + checkImage({ size, format }); + return `${root}/channel-icons/${channelID}/${hash}.${format}${size ? `?size=${size}` : ''}`; + }, Splash: (guildID, hash, format = 'webp', size) => { checkImage({ size, format }); return `${root}/splashes/${guildID}/${hash}.${format}${size ? `?size=${size}` : ''}`; From ba1c257bb202e475bdc26a093123bb65497ddfa3 Mon Sep 17 00:00:00 2001 From: Yoson Chiu Date: Fri, 4 Aug 2017 03:18:37 -0400 Subject: [PATCH 0182/1359] Fixes #1693 (#1745) * Update AudioPlayer.js * Set timestamp to 0 as well. --- src/client/voice/player/AudioPlayer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 2a264f9e6..ca0f759ff 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -77,6 +77,8 @@ class AudioPlayer extends EventEmitter { dispatcher.destroy('end'); } this.currentStream = {}; + this.streamingData.pausedTime = 0; + this.streamingData.timestamp = 0; } /** From 580e066a36ce0d0626dba7d457977d3760909e00 Mon Sep 17 00:00:00 2001 From: FireController1847 Date: Fri, 4 Aug 2017 01:21:18 -0600 Subject: [PATCH 0183/1359] Init pause variable on VoiceBroadcast (#1751) * Init pause variable on VoiceBroadcast * Move it back. Could have swore I did this. --- src/client/voice/VoiceBroadcast.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 76513d86a..beb57fd06 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -35,6 +35,11 @@ class VoiceBroadcast extends VolumeInterface { this.client = client; this._dispatchers = new Collection(); this._encoders = new Collection(); + /** + * Whether playing is paused + * @type {boolean} + */ + this.paused = false; /** * The audio transcoder that this broadcast uses * @type {Prism} From e5386e33a87ab17194c9d1d94e2c9a0ef1f08ff0 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 4 Aug 2017 03:21:16 -0500 Subject: [PATCH 0184/1359] default channel doesn't exist anymore (#1738) --- src/structures/Guild.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 12a0c6fe8..7f1d38258 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -312,15 +312,6 @@ class Guild { return this.client.voice.connections.get(this.id) || null; } - /** - * The `#general` TextChannel of the guild - * @type {TextChannel} - * @readonly - */ - get defaultChannel() { - return this.channels.get(this.id); - } - /** * The position of this guild * This is only available when using a user account. From dab28a090676661181958fc013242ed903b417be Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 4 Aug 2017 03:45:41 -0500 Subject: [PATCH 0185/1359] Stop creating package-lock.json once and for all (#1753) --- .npmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..579e0e4d9 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-json=false From e5be90256831f0612b2f5ac668d887e30ad602d7 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 4 Aug 2017 03:46:10 -0500 Subject: [PATCH 0186/1359] where we're going we don't need referrers (#1749) --- src/util/Constants.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 9c11f9e85..0a2a72e31 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -60,8 +60,6 @@ exports.DefaultOptions = { $os: process ? process.platform : 'discord.js', $browser: 'discord.js', $device: 'discord.js', - $referrer: '', - $referring_domain: '', }, version: 6, }, From 317a3523372a8ef7e66a827c59900809e624c40a Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 5 Aug 2017 15:41:00 +0200 Subject: [PATCH 0187/1359] Fix ClientUser#settings not showing up in the documentation (#1757) --- src/structures/ClientUser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 8f75a5522..d959375a3 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -78,7 +78,7 @@ class ClientUser extends User { * This is only filled when using a user account. * @type {?ClientUserSettings} */ - if (data.user_settings) this.settings = new ClientUserSettings(this, data.user_settings); + this.settings = data.user_settings ? new ClientUserSettings(this, data.user_settings) : null; /** * All of the user's guild settings From 62fc9fce6df9bca0401cdd9655c11104e8a32d3a Mon Sep 17 00:00:00 2001 From: Isabella Date: Sun, 6 Aug 2017 17:09:47 -0500 Subject: [PATCH 0188/1359] Add Attachment structure (#1731) * Add Attachment structure * Fix linter issues + @private * Fixed array sends, also added embed sends * fixed proving path to attachment * fixed incorrect name assumption from path * linting fix * ;) * im really good at this * changes as requested by gus and computer from #1459 * am a dum * update webhook#send * readonly addition to getters * i... uh... oops * farming deez commits * fix webhook split * removed some ugly * removed .every checks --- src/client/ClientDataResolver.js | 22 ++++++ src/errors/Messages.js | 2 +- src/index.js | 1 + src/structures/Attachment.js | 73 +++++++++++++++++++ src/structures/MessageEmbed.js | 22 +++++- src/structures/Webhook.js | 73 +++++++++++++++---- src/structures/interfaces/TextBasedChannel.js | 28 +++++-- 7 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 src/structures/Attachment.js diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index edb8a015f..c25abc30a 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -215,6 +215,28 @@ class ClientDataResolver { return Promise.reject(new TypeError('REQ_RESOURCE_TYPE')); } + /** + * Converts a Stream to a Buffer. + * @param {Stream} resource The stream to convert + * @returns {Promise} + */ + resolveFile(resource) { + return resource ? this.resolveBuffer(resource) + .catch(() => { + if (resource.pipe && typeof resource.pipe === 'function') { + return new Promise((resolve, reject) => { + const buffers = []; + resource.once('error', reject); + resource.on('data', data => buffers.push(data)); + resource.once('end', () => resolve(Buffer.concat(buffers))); + }); + } else { + throw new TypeError('REQ_RESOURCE_TYPE'); + } + }) : + Promise.reject(new TypeError('REQ_RESOURCE_TYPE')); + } + /** * Data that can be resolved to give an emoji identifier. This can be: * * The unicode representation of an emoji diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 43f47f370..37950d5eb 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -63,7 +63,7 @@ const Messages = { UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.', REQ_BODY_TYPE: 'The response body isn\'t a Buffer.', - REQ_RESOURCE_TYPE: 'The resource must be a string or Buffer.', + REQ_RESOURCE_TYPE: 'The resource must be a string, Buffer or a valid file stream.', IMAGE_FORMAT: format => `Invalid image format: ${format}`, IMAGE_SIZE: size => `Invalid image size: ${size}`, diff --git a/src/index.js b/src/index.js index cb109cee5..face2be00 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,7 @@ module.exports = { splitMessage: Util.splitMessage, // Structures + Attachment: require('./structures/Attachment'), Channel: require('./structures/Channel'), ClientUser: require('./structures/ClientUser'), ClientUserSettings: require('./structures/ClientUserSettings'), diff --git a/src/structures/Attachment.js b/src/structures/Attachment.js new file mode 100644 index 000000000..94357683a --- /dev/null +++ b/src/structures/Attachment.js @@ -0,0 +1,73 @@ +/** + * Represents an attachment in a message + */ +class Attachment { + constructor(file, name) { + this.file = null; + this._attach(file, name); + } + + /** + * The name of the file + * @type {?string} + * @readonly + */ + get name() { + return this.file.name; + } + + /** + * The file + * @type {?BufferResolvable|Stream} + * @readonly + */ + get attachment() { + return this.file.attachment; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} file The file + * @param {string} name The name of the file + * @returns {Attachment} This attachment + */ + setAttachment(file, name) { + this.file = { attachment: file, name }; + return this; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} attachment The file + * @returns {Attachment} This attachment + */ + setFile(attachment) { + this.file.attachment = attachment; + return this; + } + + /** + * Set the name of this attachment. + * @param {string} name The name of the image + * @returns {Attachment} This attachment + */ + setName(name) { + this.file.name = name; + return this; + } + + /** + * Set the file of this attachment. + * @param {BufferResolvable|Stream} file The file + * @param {string} name The name of the file + * @private + */ + _attach(file, name) { + if (file) { + if (typeof file === 'string') this.file = file; + else this.setAttachment(file, name); + } + } +} + +module.exports = Attachment; diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 78075c011..5f93548b7 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -1,3 +1,4 @@ +const Attachment = require('./Attachment'); const Util = require('../util/Util'); const { RangeError } = require('../errors'); @@ -129,6 +130,17 @@ class MessageEmbed { iconURL: data.footer.iconURL || data.footer.icon_url, proxyIconURL: data.footer.proxyIconURL || data.footer.proxy_icon_url, } : null; + + /** + * The files of this embed + * @type {?Object} + * @property {Array} files Files to attach + */ + if (data.files) { + for (let file of data.files) { + if (file instanceof Attachment) file = file.file; + } + } else { data.files = null; } } /** @@ -178,12 +190,15 @@ class MessageEmbed { /** * Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when * setting an embed image or author/footer icons. Only one file may be attached. - * @param {Array} files Files to attach + * @param {Array} files Files to attach * @returns {MessageEmbed} This embed */ attachFiles(files) { if (this.files) this.files = this.files.concat(files); else this.files = files; + for (let file of files) { + if (file instanceof Attachment) file = file.file; + } return this; } @@ -286,6 +301,11 @@ class MessageEmbed { return this; } + /** + * Transforms the embed object to be processed. + * @returns {Object} The raw data of this embed + * @private + */ _apiTransform() { return { title: this.title, diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index f1ee54fad..ba62b2ba0 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,8 @@ const path = require('path'); const Util = require('../util/Util'); const Embed = require('./MessageEmbed'); +const Attachment = require('./Attachment'); +const MessageEmbed = require('./MessageEmbed'); /** * Represents a webhook. @@ -82,7 +84,7 @@ class Webhook { * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here * should be replaced with plain-text - * @property {FileOptions|string} [file] A file to send with the message + * @property {FileOptions|BufferResolvable} [file] A file to send with the message * @property {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if @@ -100,7 +102,7 @@ class Webhook { * .then(message => console.log(`Sent message: ${message.content}`)) * .catch(console.error); */ - send(content, options) { + send(content, options) { // eslint-disable-line complexity if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; content = ''; @@ -108,49 +110,70 @@ class Webhook { options = {}; } - if (!options.username) options.username = this.name; + if (options instanceof Attachment) 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 Attachment); + 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 (typeof content !== 'undefined') content = Util.resolveString(content); if (content) { - if (options.disableEveryone || - (typeof options.disableEveryone === 'undefined' && this.client.options.disableEveryone) - ) { + 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.file) { - if (options.files) options.files.push(options.file); - else options.files = [options.file]; - } - if (options.files) { for (let i = 0; i < options.files.length; i++) { let file = options.files[i]; - if (typeof file === 'string') file = { attachment: file }; + if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file }; if (!file.name) { if (typeof file.attachment === 'string') { file.name = path.basename(file.attachment); } else if (file.attachment && file.attachment.path) { file.name = path.basename(file.attachment.path); + } else if (file instanceof Attachment) { + file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' }; } else { file.name = 'file.jpg'; } + } else if (file instanceof Attachment) { + file = file.file; } options.files[i] = file; } return Promise.all(options.files.map(file => - this.client.resolver.resolveBuffer(file.attachment).then(buffer => { - file.file = buffer; + this.client.resolver.resolveFile(file.attachment).then(resource => { + file.file = resource; return file; }) )).then(files => this.client.api.webhooks(this.id, this.token).post({ @@ -161,6 +184,26 @@ class Webhook { })); } + 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)); + }); + } + return this.client.api.webhooks(this.id, this.token).post({ data: options, query: { wait: true }, diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index e532b960c..a50609d2c 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -3,6 +3,8 @@ const MessageCollector = require('../MessageCollector'); const Shared = require('../shared'); const Collection = require('../../util/Collection'); const Snowflake = require('../../util/Snowflake'); +const Attachment = require('../../structures/Attachment'); +const MessageEmbed = require('../../structures/MessageEmbed'); const { Error, RangeError, TypeError } = require('../../errors'); /** @@ -39,7 +41,7 @@ class TextBasedChannel { * (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[]} [files] Files to send with the message + * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if * it exceeds the character limit. If an object is provided, these are the options for splitting the message @@ -72,7 +74,7 @@ class TextBasedChannel { * .then(message => console.log(`Sent message: ${message.content}`)) * .catch(console.error); */ - send(content, options) { + send(content, options) { // eslint-disable-line complexity if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; content = ''; @@ -80,6 +82,18 @@ class TextBasedChannel { options = {}; } + if (options instanceof MessageEmbed) options = { embed: options }; + if (options instanceof Attachment) 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 Attachment); + if (attachments.length) { + options = { files: attachments }; + if (content instanceof Array) content = ''; + } + } + if (!options.content) options.content = content; if (options.embed && options.embed.files) { @@ -90,22 +104,26 @@ class TextBasedChannel { if (options.files) { for (let i = 0; i < options.files.length; i++) { let file = options.files[i]; - if (typeof file === 'string') file = { attachment: file }; + if (typeof file === 'string' || Buffer.isBuffer(file)) file = { attachment: file }; if (!file.name) { if (typeof file.attachment === 'string') { file.name = path.basename(file.attachment); } else if (file.attachment && file.attachment.path) { file.name = path.basename(file.attachment.path); + } else if (file instanceof Attachment) { + file = { attachment: file.file, name: path.basename(file.file) || 'file.jpg' }; } else { file.name = 'file.jpg'; } + } else if (file instanceof Attachment) { + file = file.file; } options.files[i] = file; } return Promise.all(options.files.map(file => - this.client.resolver.resolveBuffer(file.attachment).then(buffer => { - file.file = buffer; + this.client.resolver.resolveFile(file.attachment).then(resource => { + file.file = resource; return file; }) )).then(files => { From 3682918df979a1dcbfc99acbd191209ae58406b9 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 6 Aug 2017 17:57:25 -0400 Subject: [PATCH 0189/1359] Update typings --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index 8e56ab9ac..0967675a2 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit 8e56ab9ac1284fdf9b78b153c78e62d175f99b4b +Subproject commit 0967675a2f8e6fa46ab543f955af82a82230d17a From 54ff8ce59683585adb4404b815e203ddc918c45c Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 6 Aug 2017 18:15:34 -0400 Subject: [PATCH 0190/1359] Fix tern file --- .tern-project | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tern-project b/.tern-project index 8f37bf06e..3c8015ce6 100644 --- a/.tern-project +++ b/.tern-project @@ -15,7 +15,7 @@ "strong": true }, "webpack": { - "configPath": "./webpack.config.js", + "configPath": "./webpack.config.js" } } } From 25dd3bc29e381885ff896b0c2ce8dc2c13ebc995 Mon Sep 17 00:00:00 2001 From: Raphael Date: Thu, 10 Aug 2017 01:17:12 +0200 Subject: [PATCH 0191/1359] updated docs for .toString() so it now uses send instead sendMessage in example (#1761) --- src/structures/ReactionEmoji.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index c8e67b570..f550544c6 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -38,7 +38,7 @@ class ReactionEmoji { * Creates the text required to form a graphical emoji on Discord. * @example * // Send the emoji used in a reaction to the channel the reaction is part of - * reaction.message.channel.sendMessage(`The emoji used is ${reaction.emoji}`); + * reaction.message.channel.send(`The emoji used is ${reaction.emoji}`); * @returns {string} */ toString() { From a49709d329b6b13b468044ba0c98d7c15f4fafdb Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 10 Aug 2017 01:18:13 +0200 Subject: [PATCH 0192/1359] Readded permissions to Role#edit's payload (#1760) --- src/structures/Role.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index c9ab6075e..2c9bbbbfe 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -205,9 +205,10 @@ class Role { return this.client.api.guilds[this.guild.id].roles[this.id].patch({ data: { name: data.name || this.name, - position: typeof data.position !== 'undefined' ? data.position : this.position, color: Util.resolveColor(data.color || this.color), hoist: typeof data.hoist !== 'undefined' ? data.hoist : this.hoist, + position: typeof data.position !== 'undefined' ? data.position : this.position, + permissions: data.permissions, mentionable: typeof data.mentionable !== 'undefined' ? data.mentionable : this.mentionable, }, reason, From 48b69c6e2f96ae6487ba178bbb211fc80a7f51c2 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 10 Aug 2017 01:21:34 +0200 Subject: [PATCH 0193/1359] No longer double increment the reaction count when the client reacts (#1755) --- src/structures/Message.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 52de85ddd..97584d84d 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -548,8 +548,10 @@ class Message { reaction = new MessageReaction(this, emoji, 0, user.id === this.client.user.id); this.reactions.set(emojiID, reaction); } - if (!reaction.users.has(user.id)) reaction.users.set(user.id, user); - reaction.count++; + if (!reaction.users.has(user.id)) { + reaction.users.set(user.id, user); + reaction.count++; + } return reaction; } From 87cdad332c6b8979d5109ba110d8c0c5c1f2568f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 10 Aug 2017 01:22:57 +0200 Subject: [PATCH 0194/1359] Improved Guild#createChannel, added RoleResolvable and fixed a bit of Emoji stuff (#1754) * Made creating channels with overwrites nicer and added ClientDataResolver#resolveRole * Renamed ChannelPermissionOverwrites to ChannelCreationOverwrites * Added RoleResolvables everywhere possible * Fixed Emoji#setName resetting restricted roles and Emoji#equals Which will lead to emojis not to update when roles are being added removed. --- src/client/ClientDataResolver.js | 22 +++++++++ src/structures/Emoji.js | 30 ++++++++---- src/structures/Guild.js | 82 ++++++++++++++++++++++++-------- src/structures/GuildMember.js | 47 +++++++++--------- src/structures/Role.js | 4 +- 5 files changed, 132 insertions(+), 53 deletions(-) diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index c25abc30a..ba7df252a 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -8,6 +8,7 @@ const Message = require('../structures/Message'); const Guild = require('../structures/Guild'); const Channel = require('../structures/Channel'); const GuildMember = require('../structures/GuildMember'); +const Role = require('../structures/Role'); const Emoji = require('../structures/Emoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); const { Error, TypeError } = require('../errors'); @@ -101,6 +102,27 @@ class ClientDataResolver { return guild.members.get(user.id) || null; } + /** + * Data that can be resolved to a Role object. This can be: + * * A Role + * * A Snowflake + * @typedef {Role|Snowflake} RoleResolvable + */ + + /** + * Resolves a RoleResolvable to a Role object. + * @param {GuildResolvable} guild The guild that this role is part of + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Role} + */ + resolveRole(guild, role) { + if (role instanceof Role) return role; + guild = this.resolveGuild(guild); + if (!guild) return null; + if (typeof role === 'string') return guild.roles.get(role); + return null; + } + /** * Data that can be resolved to give a Channel object. This can be: * * A Channel object diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 8391ad37b..1920d6fdc 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -106,7 +106,7 @@ class Emoji { * Data for editing an emoji. * @typedef {Object} EmojiEditData * @property {string} [name] The name of the emoji - * @property {Collection|Array} [roles] Roles to restrict emoji to + * @property {Collection|RoleResolvable[]} [roles] Roles to restrict emoji to */ /** @@ -124,7 +124,7 @@ class Emoji { return this.client.api.guilds(this.guild.id).emojis(this.id) .patch({ data: { name: data.name, - roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : [], + roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : undefined, }, reason }) .then(() => this); } @@ -150,13 +150,18 @@ class Emoji { /** * Add multiple roles to the list of roles that can use this emoji. - * @param {Role[]} roles Roles to add + * @param {Collection|RoleResolvable[]} roles Roles to add * @returns {Promise} */ addRestrictedRoles(roles) { const newRoles = new Collection(this.roles); - for (const role of roles) { - if (this.guild.roles.has(role.id)) newRoles.set(role.id, role); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + newRoles.set(role.id, role); } return this.edit({ roles: newRoles }); } @@ -172,12 +177,17 @@ class Emoji { /** * Remove multiple roles from the list of roles that can use this emoji. - * @param {Role[]} roles Roles to remove + * @param {Collection|RoleResolvable[]} roles Roles to remove * @returns {Promise} */ removeRestrictedRoles(roles) { const newRoles = new Collection(this.roles); - for (const role of roles) { + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); + } if (newRoles.has(role.id)) newRoles.delete(role.id); } return this.edit({ roles: newRoles }); @@ -206,12 +216,14 @@ class Emoji { other.id === this.id && other.name === this.name && other.managed === this.managed && - other.requiresColons === this.requiresColons + other.requiresColons === this.requiresColons && + other._roles === this._roles ); } else { return ( other.id === this.id && - other.name === this.name + other.name === this.name && + other._roles === this._roles ); } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 7f1d38258..ff7f5d5bd 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -508,7 +508,7 @@ class Guild { * @param {string} options.accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the * bot's application * @param {string} [options.nick] Nickname to give the member (requires `MANAGE_NICKNAMES`) - * @param {Collection|Role[]|Snowflake[]} [options.roles] Roles to add to the member + * @param {Collection|RoleResolvable[]} [options.roles] Roles to add to the member * (requires `MANAGE_ROLES`) * @param {boolean} [options.mute] Whether the member should be muted (requires `MUTE_MEMBERS`) * @param {boolean} [options.deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`) @@ -518,9 +518,14 @@ class Guild { if (this.members.has(user.id)) return Promise.resolve(this.members.get(user.id)); options.access_token = options.accessToken; if (options.roles) { - const roles = options.roles; - if (roles instanceof Collection || (roles instanceof Array && roles[0] instanceof Role)) { - options.roles = roles.map(role => role.id); + const roles = []; + for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) { + role = this.client.resolver.resolveRole(this, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + roles.push(role.id); } } return this.client.api.guilds(this.id).members(user.id).put({ data: options }) @@ -898,12 +903,21 @@ class Guild { if (!this.client.user.bot) this.client.syncGuilds([this]); } + /** + * Can be used to overwrite permissions when creating a channel. + * @typedef {Object} ChannelCreationOverwrites + * @property {PermissionResolveable[]|number} [allow] The permissions to allow + * @property {PermissionResolveable[]|number} [deny] The permissions to deny + * @property {RoleResolveable|UserResolvable} id ID of the group or member this overwrite is for + */ + /** * Creates a new channel in the guild. * @param {string} name The name of the new channel * @param {string} type The type of the new channel, either `text` or `voice` - * @param {Object} options Options - * @param {Array} [options.overwrites] Permission overwrites to apply to the new channel + * @param {Object} [options={}] Options + * @param {Array} [options.overwrites] Permission overwrites + * to apply to the new channel * @param {string} [options.reason] Reason for creating this channel * @returns {Promise} * @example @@ -914,13 +928,30 @@ class Guild { */ createChannel(name, type, { overwrites, reason } = {}) { if (overwrites instanceof Collection || overwrites instanceof Array) { - overwrites = overwrites.map(overwrite => ({ - allow: overwrite.allow || overwrite._allowed, - deny: overwrite.deny || overwrite._denied, - type: overwrite.type, - id: overwrite.id, - })); + overwrites = overwrites.map(overwrite => { + let allow = overwrite.allow || overwrite._allowed; + let deny = overwrite.deny || overwrite._denied; + if (allow instanceof Array) allow = Permissions.resolve(allow); + if (deny instanceof Array) deny = Permissions.resolve(deny); + + const role = this.client.resolver.resolveRole(this, overwrite.id); + if (role) { + overwrite.id = role.id; + overwrite.type = 'role'; + } else { + overwrite.id = this.client.resolver.resolveUserID(overwrite.id); + overwrite.type = 'member'; + } + + return { + allow, + deny, + type: overwrite.type, + id: overwrite.id, + }; + }); } + return this.client.api.guilds(this.id).channels.post({ data: { name, type, permission_overwrites: overwrites, @@ -1005,7 +1036,7 @@ class Guild { * @param {BufferResolvable|Base64Resolvable} attachment The image for the emoji * @param {string} name The name for the emoji * @param {Object} [options] Options - * @param {Collection|Role[]} [options.roles] Roles to limit the emoji to + * @param {Collection|RoleResolvable[]} [options.roles] Roles to limit the emoji to * @param {string} [options.reason] Reason for creating the emoji * @returns {Promise} The created emoji * @example @@ -1022,16 +1053,27 @@ class Guild { createEmoji(attachment, name, { roles, reason } = {}) { if (typeof attachment === 'string' && attachment.startsWith('data:')) { const data = { image: attachment, name }; - if (roles) data.roles = roles.map(r => r.id ? r.id : r); + if (roles) { + data.roles = []; + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + data.roles.push(role.id); + } + } + return this.client.api.guilds(this.id).emojis.post({ data, reason }) .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); - } else { - return this.client.resolver.resolveBuffer(attachment) - .then(data => { - const dataURI = this.client.resolver.resolveBase64(data); - return this.createEmoji(dataURI, name, roles); - }); } + + return this.client.resolver.resolveBuffer(attachment) + .then(data => { + const dataURI = this.client.resolver.resolveBase64(data); + return this.createEmoji(dataURI, name, { roles, reason }); + }); } /** diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index f6a10cbe9..b91930e51 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -324,7 +324,7 @@ class GuildMember { * The data for editing a guild member. * @typedef {Object} GuildMemberEditData * @property {string} [nick] The nickname to set for the member - * @property {Collection|Role[]|Snowflake[]} [roles] The roles or role IDs to apply + * @property {Collection|RoleResolvable[]} [roles] The roles or role IDs to apply * @property {boolean} [mute] Whether or not the member should be muted * @property {boolean} [deaf] Whether or not the member should be deafened * @property {ChannelResolvable} [channel] Channel to move member to (if they are connected to voice) @@ -384,7 +384,7 @@ class GuildMember { /** * Sets the roles applied to the member. - * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to apply + * @param {Collection|RoleResolvable[]} roles The roles or role IDs to apply * @param {string} [reason] Reason for applying the roles * @returns {Promise} */ @@ -394,12 +394,12 @@ class GuildMember { /** * Adds a single role to the member. - * @param {Role|Snowflake} role The role or ID of the role to add + * @param {RoleResolvable} role The role or ID of the role to add * @param {string} [reason] Reason for adding the role * @returns {Promise} */ addRole(role, reason) { - if (!(role instanceof Role)) role = this.guild.roles.get(role); + role = this.client.resolver.resolveRole(this.guild, role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); if (this._roles.includes(role.id)) return Promise.resolve(this); return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) @@ -409,30 +409,33 @@ class GuildMember { /** * Adds multiple roles to the member. - * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to add + * @param {Collection|RoleResolvable[]} roles The roles or role IDs to add * @param {string} [reason] Reason for adding the roles * @returns {Promise} */ addRoles(roles, reason) { - let allRoles; - if (roles instanceof Collection) { - allRoles = this._roles.slice(); - for (const role of roles.values()) allRoles.push(role.id ? role.id : role); - } else { - allRoles = this._roles.concat(roles.map(r => r.id ? r.id : r)); + let allRoles = this._roles.slice(); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); + } + allRoles.push(role.id); } return this.edit({ roles: allRoles }, reason); } /** * Removes a single role from the member. - * @param {Role|Snowflake} role The role or ID of the role to remove + * @param {RoleResolvable} role The role or ID of the role to remove * @param {string} [reason] Reason for removing the role * @returns {Promise} */ removeRole(role, reason) { - if (!(role instanceof Role)) role = this.guild.roles.get(role); + role = this.client.resolver.resolveRole(this.guild, role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); + if (!this._roles.includes(role.id)) return Promise.resolve(this); return this.client.api.guilds(this.guild.id).members(this.user.id).roles(role.id) .delete({ reason }) .then(() => this); @@ -440,22 +443,20 @@ class GuildMember { /** * Removes multiple roles from the member. - * @param {Collection|Role[]|Snowflake[]} roles The roles or role IDs to remove + * @param {Collection|RoleResolvable[]} roles The roles or role IDs to remove * @param {string} [reason] Reason for removing the roles * @returns {Promise} */ removeRoles(roles, reason) { const allRoles = this._roles.slice(); - if (roles instanceof Collection) { - for (const role of roles.values()) { - const index = allRoles.indexOf(role.id); - if (index >= 0) allRoles.splice(index, 1); - } - } else { - for (const role of roles) { - const index = allRoles.indexOf(role instanceof Role ? role.id : role); - if (index >= 0) allRoles.splice(index, 1); + for (let role of roles instanceof Collection ? roles.values() : roles) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) { + return Promise.reject(new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true)); } + const index = allRoles.indexOf(role.id); + if (index >= 0) allRoles.splice(index, 1); } return this.edit({ roles: allRoles }, reason); } diff --git a/src/structures/Role.js b/src/structures/Role.js index 2c9bbbbfe..c6dfb611d 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -169,11 +169,13 @@ class Role { /** * Compares this role's position to another role's. - * @param {Role} role Role to compare to this one + * @param {RoleResolvable} role Role to compare to this one * @returns {number} Negative number if the this role's position is lower (other role's is higher), * positive number if the this one is higher (other's is lower), 0 if equal */ comparePositionTo(role) { + role = this.client.resolver.resolveRole(this.guild, role); + if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); return this.constructor.comparePositions(this, role); } From fa5c4efa2bc545bc4df6f449dde343a6dcfdcfe2 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 10 Aug 2017 01:25:24 +0200 Subject: [PATCH 0195/1359] Fixed a bunch of ClientUserGuildSettings stuff and its docs (#1758) --- src/structures/ClientUser.js | 4 +--- src/structures/ClientUserChannelOverride.js | 6 ++---- src/structures/ClientUserGuildSettings.js | 23 ++++++++++++++------- src/structures/Guild.js | 12 ++++++++--- src/structures/GuildChannel.js | 12 ++++++----- src/structures/User.js | 2 +- src/util/Constants.js | 4 ++-- 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index d959375a3..c26292fc1 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -88,9 +88,7 @@ class ClientUser extends User { this.guildSettings = new Collection(); if (data.user_guild_settings) { for (const settings of data.user_guild_settings) { - settings.client = this.client; - const guild = this.client.guilds.get(settings.guild_id); - this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(settings, guild)); + this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(settings, this.client)); } } } diff --git a/src/structures/ClientUserChannelOverride.js b/src/structures/ClientUserChannelOverride.js index 72414fee5..c9fe03de7 100644 --- a/src/structures/ClientUserChannelOverride.js +++ b/src/structures/ClientUserChannelOverride.js @@ -4,8 +4,7 @@ const Constants = require('../util/Constants'); * A wrapper around the ClientUser's channel overrides. */ class ClientUserChannelOverride { - constructor(user, data) { - this.user = user; + constructor(data) { this.patch(data); } @@ -14,8 +13,7 @@ class ClientUserChannelOverride { * @param {Object} data Data to patch this with */ patch(data) { - for (const key of Object.keys(Constants.UserChannelOverrideMap)) { - const value = Constants.UserChannelOverrideMap[key]; + for (const [key, value] of Object.entries(Constants.UserChannelOverrideMap)) { if (!data.hasOwnProperty(key)) continue; if (typeof value === 'function') { this[value.name] = value(data[key]); diff --git a/src/structures/ClientUserGuildSettings.js b/src/structures/ClientUserGuildSettings.js index 75d2f8332..dc7fd1ca6 100644 --- a/src/structures/ClientUserGuildSettings.js +++ b/src/structures/ClientUserGuildSettings.js @@ -6,9 +6,19 @@ const ClientUserChannelOverride = require('./ClientUserChannelOverride'); * A wrapper around the ClientUser's guild settings. */ class ClientUserGuildSettings { - constructor(data, guild) { - this.guild = guild; - this.client = data.client; + constructor(data, client) { + /** + * The client that created the instance of the the user + * @name ClientUserGuildSettings#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + /** + * The ID of the guild this settings are for + * @type {Snowflake} + */ + this.guildID = data.guild_id; this.channelOverrides = new Collection(); this.patch(data); } @@ -18,13 +28,12 @@ class ClientUserGuildSettings { * @param {Object} data Data to patch this with */ patch(data) { - for (const key of Object.keys(Constants.UserGuildSettingsMap)) { - const value = Constants.UserGuildSettingsMap[key]; + for (const [key, value] of Object.entries(Constants.UserGuildSettingsMap)) { if (!data.hasOwnProperty(key)) continue; if (key === 'channel_overrides') { for (const channel of data[key]) { this.channelOverrides.set(channel.channel_id, - new ClientUserChannelOverride(this.client.user, channel)); + new ClientUserChannelOverride(channel)); } } else if (typeof value === 'function') { this[value.name] = value(data[key]); @@ -41,7 +50,7 @@ class ClientUserGuildSettings { * @returns {Promise} */ update(name, value) { - return this.guild.client.api.guilds(this.guild.id).settings.patch({ data: { [name]: value } }); + return this.client.api.users('@me').guilds(this.guildID).settings.patch({ data: { [name]: value } }); } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index ff7f5d5bd..76f91a525 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -316,6 +316,7 @@ class Guild { * The position of this guild * This is only available when using a user account. * @type {?number} + * @readonly */ get position() { if (this.client.user.bot) return null; @@ -327,6 +328,7 @@ class Guild { * Whether the guild is muted * This is only available when using a user account. * @type {?boolean} + * @readonly */ get muted() { if (this.client.user.bot) return null; @@ -341,7 +343,8 @@ class Guild { * The type of message that should notify you * one of `EVERYTHING`, `MENTIONS`, `NOTHING` * This is only available when using a user account. - * @type {string} + * @type {?string} + * @readonly */ get messageNotifications() { if (this.client.user.bot) return null; @@ -355,7 +358,8 @@ class Guild { /** * Whether to receive mobile push notifications * This is only available when using a user account. - * @type {boolean} + * @type {?boolean} + * @readonly */ get mobilePush() { if (this.client.user.bot) return null; @@ -369,9 +373,11 @@ class Guild { /** * Whether to suppress everyone messages * This is only available when using a user account. - * @type {boolean} + * @type {?boolean} + * @readonly */ get suppressEveryone() { + if (this.client.user.bot) return null; try { return this.client.user.guildSettings.get(this.id).suppressEveryone; } catch (err) { diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 20b18376a..20356722d 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -365,10 +365,11 @@ class GuildChannel extends Channel { } /** - * Whether the channel is muted - * This is only available when using a user account. - * @type {boolean} - */ + * Whether the channel is muted + * This is only available when using a user account. + * @type {?boolean} + * @readonly + */ get muted() { if (this.client.user.bot) return null; try { @@ -382,7 +383,8 @@ class GuildChannel extends Channel { * The type of message that should notify you * one of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` * This is only available when using a user account. - * @type {string} + * @type {?string} + * @readonly */ get messageNotifications() { if (this.client.user.bot) return null; diff --git a/src/structures/User.js b/src/structures/User.js index 022f51f60..8b0daa3fc 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -14,7 +14,7 @@ class User { /** * The client that created the instance of the the user * @name User#client - * @type {} + * @type {Client} * @readonly */ Object.defineProperty(this, 'client', { value: client }); diff --git a/src/util/Constants.js b/src/util/Constants.js index 0a2a72e31..fddd8934f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -534,14 +534,14 @@ exports.UserChannelOverrideMap = { /** * The type of message that should notify you * one of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` - * @name ClientUserChannelOverrides#messageNotifications + * @name ClientUserChannelOverride#messageNotifications * @type {string} */ return exports.MessageNotificationTypes[type]; }, /** * Whether the guild is muted or not - * @name ClientUserChannelOverrides#muted + * @name ClientUserChannelOverride#muted * @type {boolean} */ muted: 'muted', From d9262f2682b4573dd5e42b42a0ca429b08bce519 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 10 Aug 2017 20:31:10 +0200 Subject: [PATCH 0196/1359] Added and fixed a bunch of docs stuff (#1767) - Fixed a common copy paste fail `the the ` in various places - Apparently I can't type Resolvable correctly, Fixed that wherever applicable - Documented GroupDMChannel#nicks so that it will be displayed on the docs - GroupDMChannel#icon is nullable - Removed empty InviteOptions typdef, as its properties are now documented in GuildChannel#createInvite - MessageMentions#channels is no longer nullable - RoleData#permissions takes a PermissionResolvable or an array of them - Webhook#avatar is nullable - Added HTTPOptions typedef and added it to ClientOptions typedef - ClientUserChannelOverride#muted is for a channel and not a guild directly --- src/client/voice/VoiceConnection.js | 2 +- src/structures/ClientUser.js | 2 +- src/structures/ClientUserGuildSettings.js | 2 +- src/structures/GroupDMChannel.js | 18 +++++++++-------- src/structures/Guild.js | 8 ++++---- src/structures/GuildChannel.js | 8 +------- src/structures/MessageMentions.js | 2 +- src/structures/Role.js | 2 +- src/structures/User.js | 2 +- src/structures/UserProfile.js | 2 +- src/structures/Webhook.js | 2 +- src/structures/interfaces/Collector.js | 2 +- src/util/Constants.js | 24 ++++++++++++++++------- 13 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index d15c8ffbc..18c5ee084 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -165,7 +165,7 @@ class VoiceConnection extends EventEmitter { } /** - * Set the token and endpoint required to connect to the the voice servers. + * Set the token and endpoint required to connect to the voice servers. * @param {string} token The voice token * @param {string} endpoint The voice endpoint * @returns {void} diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index c26292fc1..e533e2284 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -346,7 +346,7 @@ class ClientUser extends User { * @property {string} [accessToken] Access token to use to add a user to the Group DM * (only available if a bot is creating the DM) * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) - * @property {string} [id] If no user resolveable is provided and you want to assign nicknames + * @property {string} [id] If no user resolvable is provided and you want to assign nicknames * you must provide user ids instead */ diff --git a/src/structures/ClientUserGuildSettings.js b/src/structures/ClientUserGuildSettings.js index dc7fd1ca6..3826c019c 100644 --- a/src/structures/ClientUserGuildSettings.js +++ b/src/structures/ClientUserGuildSettings.js @@ -8,7 +8,7 @@ const ClientUserChannelOverride = require('./ClientUserChannelOverride'); class ClientUserGuildSettings { constructor(data, client) { /** - * The client that created the instance of the the user + * The client that created the instance of the ClientUserGuildSettings * @name ClientUserGuildSettings#client * @type {Client} * @readonly diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 9060fa932..0511cf050 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -49,7 +49,7 @@ class GroupDMChannel extends Channel { /** * A hash of this Group DM icon - * @type {string} + * @type {?string} */ this.icon = data.icon; @@ -71,11 +71,13 @@ class GroupDMChannel extends Channel { */ this.applicationID = data.application_id; - /** - * Nicknames for group members - * @type {?Collection} - */ - if (data.nicks) this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick])); + if (data.nicks) { + /** + * Nicknames for group members + * @type {?Collection} + */ + this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick])); + } if (!this.recipients) { /** @@ -181,7 +183,7 @@ class GroupDMChannel extends Channel { /** * Adds an user to this Group DM. * @param {Object} options Options for this method - * @param {UserResolveable} options.user User to add to this Group DM + * @param {UserResolvable} options.user User to add to this Group DM * @param {string} [options.accessToken] Access token to use to add the user to this Group DM * (only available under a bot account) * @param {string} [options.nick] Permanent nickname to give the user (only available under a bot account) @@ -198,7 +200,7 @@ class GroupDMChannel extends Channel { /** * Removes an user from this Group DM. - * @param {UserResolveable} user User to remove + * @param {UserResolvable} user User to remove * @returns {Promise} */ removeUser(user) { diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 76f91a525..1b36eb2f4 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -25,7 +25,7 @@ const { Error, TypeError } = require('../errors'); class Guild { constructor(client, data) { /** - * The client that created the instance of the the guild + * The client that created the instance of the guild * @name Guild#client * @type {Client} * @readonly @@ -912,9 +912,9 @@ class Guild { /** * Can be used to overwrite permissions when creating a channel. * @typedef {Object} ChannelCreationOverwrites - * @property {PermissionResolveable[]|number} [allow] The permissions to allow - * @property {PermissionResolveable[]|number} [deny] The permissions to deny - * @property {RoleResolveable|UserResolvable} id ID of the group or member this overwrite is for + * @property {PermissionResolvable[]|number} [allow] The permissions to allow + * @property {PermissionResolvable[]|number} [deny] The permissions to deny + * @property {RoleResolvable|UserResolvable} id ID of the group or member this overwrite is for */ /** diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 20356722d..71c321c1f 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -274,15 +274,9 @@ class GuildChannel extends Channel { return this.edit({ topic }, reason); } - /** - * Options given when creating a guild channel invite. - * @typedef {Object} InviteOptions - - */ - /** * Create an invite to this guild channel. - * @param {InviteOptions} [options={}] Options for the invite + * @param {Object} [options={}] Options for the invite * @param {boolean} [options.temporary=false] Whether members that joined via the invite should be automatically * kicked after 24 hours if they have not yet received a role * @param {number} [options.maxAge=86400] How long the invite should last (in seconds, 0 for forever) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 85fc764cb..b54787706 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -103,7 +103,7 @@ class MessageMentions { /** * Any channels that were mentioned - * @type {?Collection} + * @type {Collection} * @readonly */ get channels() { diff --git a/src/structures/Role.js b/src/structures/Role.js index c6dfb611d..797bb7121 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -186,7 +186,7 @@ class Role { * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number * @property {boolean} [hoist] Whether or not the role should be hoisted * @property {number} [position] The position of the role - * @property {string[]} [permissions] The permissions of the role + * @property {PermissionResolvable|PermissionResolvable[]} [permissions] The permissions of the role * @property {boolean} [mentionable] Whether or not the role should be mentionable */ diff --git a/src/structures/User.js b/src/structures/User.js index 8b0daa3fc..0a32799c5 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -12,7 +12,7 @@ const { Error } = require('../errors'); class User { constructor(client, data) { /** - * The client that created the instance of the the user + * The client that created the instance of the user * @name User#client * @type {Client} * @readonly diff --git a/src/structures/UserProfile.js b/src/structures/UserProfile.js index c89d62c8a..f2c9cea03 100644 --- a/src/structures/UserProfile.js +++ b/src/structures/UserProfile.js @@ -14,7 +14,7 @@ class UserProfile { this.user = user; /** - * The client that created the instance of the the UserProfile. + * The client that created the instance of the UserProfile. * @name UserProfile#client * @type {Client} * @readonly diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index ba62b2ba0..a42c3396b 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -40,7 +40,7 @@ class Webhook { /** * The avatar for the webhook - * @type {string} + * @type {?string} */ this.avatar = data.avatar; diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 790f7e9ec..017c3797d 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -186,7 +186,7 @@ class Collector extends EventEmitter { collect() {} /** - * Handles incoming events from the the `handleDispose`. Returns null if the event should not + * Handles incoming events from the `handleDispose`. Returns null if the event should not * be disposed, or returns the key that should be removed. * @see Collector#handleDispose * @param {...*} args Any args the event listener emits diff --git a/src/util/Constants.js b/src/util/Constants.js index fddd8934f..0238ed483 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -30,6 +30,7 @@ const { Error, RangeError } = require('../errors'); * 100% certain you don't need, as many are important, but not obviously so. The safest one to disable with the * most impact is typically `TYPING_START`. * @property {WebsocketOptions} [ws] Options for the WebSocket + * @property {HTTPOptions} [http] HTTP options */ exports.DefaultOptions = { apiRequestMethod: 'sequential', @@ -63,6 +64,15 @@ exports.DefaultOptions = { }, version: 6, }, + + /** + * HTTP options + * @typedef {Object} HTTPOptions + * @property {number} [version=7] API version to use + * @property {string} [api='https://discordapp.com/api'] Base url of the API + * @property {string} [cdn='https://cdn.discordapp.com'] Base url of the CDN + * @property {string} [invite='https://discord.gg'] Base url of invites + */ http: { version: 7, api: 'https://discordapp.com/api', @@ -469,8 +479,8 @@ exports.UserSettingsMap = { explicit_content_filter: function explicitContentFilter(type) { // eslint-disable-line func-name-matching /** - * Safe direct messaging; force people's messages with images to be scanned before they are sent to you - * one of `DISABLED`, `NON_FRIENDS`, `FRIENDS_AND_NON_FRIENDS` + * Safe direct messaging; force people's messages with images to be scanned before they are sent to you. + * One of `DISABLED`, `NON_FRIENDS`, `FRIENDS_AND_NON_FRIENDS` * @name ClientUserSettings#explicitContentFilter * @type {string} */ @@ -496,8 +506,8 @@ exports.UserSettingsMap = { exports.UserGuildSettingsMap = { message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching /** - * The type of message that should notify you - * one of `EVERYTHING`, `MENTIONS`, `NOTHING` + * The type of message that should notify you. + * One of `EVERYTHING`, `MENTIONS`, `NOTHING` * @name ClientUserGuildSettings#messageNotifications * @type {string} */ @@ -532,15 +542,15 @@ exports.UserGuildSettingsMap = { exports.UserChannelOverrideMap = { message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching /** - * The type of message that should notify you - * one of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` + * The type of message that should notify you. + * One of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` * @name ClientUserChannelOverride#messageNotifications * @type {string} */ return exports.MessageNotificationTypes[type]; }, /** - * Whether the guild is muted or not + * Whether the channel is muted or not * @name ClientUserChannelOverride#muted * @type {boolean} */ From 3c7869c1b7c04af72e37bb151290299db50cdc76 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 10 Aug 2017 13:31:45 -0500 Subject: [PATCH 0197/1359] update retry case for 5xx (#1765) --- src/client/rest/handlers/RequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/rest/handlers/RequestHandler.js b/src/client/rest/handlers/RequestHandler.js index 44427c80f..d1dea22e3 100644 --- a/src/client/rest/handlers/RequestHandler.js +++ b/src/client/rest/handlers/RequestHandler.js @@ -45,7 +45,7 @@ class RequestHandler { if (err.status === 429) { this.queue.unshift(item); finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset); - } else if (err.status === 500) { + } else if (err.status >= 500 && err.status < 600) { this.queue.unshift(item); finish(1e3 + this.client.options.restTimeOffset); } else { From 3ba224900f99199ecd3179070f587ad2a67ae19b Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Fri, 11 Aug 2017 10:09:06 -0700 Subject: [PATCH 0198/1359] new application stuff very hype (#1764) * application stuff, more to come * docstrings * Update Message.js --- src/client/Client.js | 4 +-- src/index.js | 3 +- ...th2Application.js => ClientApplication.js} | 31 +++++++++++++++- src/structures/Message.js | 16 +++++++++ src/util/Constants.js | 35 +++++++++---------- 5 files changed, 65 insertions(+), 24 deletions(-) rename src/structures/{OAuth2Application.js => ClientApplication.js} (80%) diff --git a/src/client/Client.js b/src/client/Client.js index 69986e071..ae40d6774 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -16,7 +16,7 @@ const VoiceRegion = require('../structures/VoiceRegion'); const Webhook = require('../structures/Webhook'); const User = require('../structures/User'); const Invite = require('../structures/Invite'); -const OAuth2Application = require('../structures/OAuth2Application'); +const ClientApplication = require('../structures/ClientApplication'); const ShardClientUtil = require('../sharding/ShardClientUtil'); const VoiceBroadcast = require('./voice/VoiceBroadcast'); const { Error, TypeError, RangeError } = require('../errors'); @@ -415,7 +415,7 @@ class Client extends EventEmitter { */ fetchApplication(id = '@me') { return this.api.oauth2.applications(id).get() - .then(app => new OAuth2Application(this, app)); + .then(app => new ClientApplication(this, app)); } /** diff --git a/src/index.js b/src/index.js index face2be00..217bb1494 100644 --- a/src/index.js +++ b/src/index.js @@ -46,8 +46,7 @@ module.exports = { MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), - OAuth2Application: require('./structures/OAuth2Application'), - ClientOAuth2Application: require('./structures/OAuth2Application'), + ClientApplication: require('./structures/ClientApplication'), PartialGuild: require('./structures/PartialGuild'), PartialGuildChannel: require('./structures/PartialGuildChannel'), PermissionOverwrites: require('./structures/PermissionOverwrites'), diff --git a/src/structures/OAuth2Application.js b/src/structures/ClientApplication.js similarity index 80% rename from src/structures/OAuth2Application.js rename to src/structures/ClientApplication.js index 15297f60e..6e00c654e 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/ClientApplication.js @@ -126,7 +126,36 @@ class OAuth2Application { */ iconURL({ format, size } = {}) { if (!this.icon) return null; - return Constants.Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.icon, format, size); + return Constants.Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.icon, { format, size }); + } + + /** + * Get rich presence assets + * @returns {Promise} + */ + fetchAssets() { + return this.client.api.applications(this.id).assets.get() + .then(assets => assets.map(a => ({ + id: a.id, + name: a.name, + type: Object.keys(Constants.ClientApplicationAssetTypes)[a.type - 1], + }))); + } + + /** + * Create a rich presence asset + * @param {string} name Name of the asset + * @param {Base64Resolvable} data Data of the asset + * @param {string} type Type of the asset. `big`, or `small` + * @returns {Promise} + */ + createAsset(name, data, type) { + return this.client.resolveBase64(data).then(b64 => + this.client.api.applications(this.id).assets.post({ data: { + name, + data: b64, + type: Constants.ClientApplicationAssetTypes[type.toUpperCase()], + } })); } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index 97584d84d..440552f95 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -3,6 +3,7 @@ const Attachment = require('./MessageAttachment'); const Embed = require('./MessageEmbed'); const MessageReaction = require('./MessageReaction'); const ReactionCollector = require('./ReactionCollector'); +const ClientApplication = require('./ClientApplication'); const Util = require('../util/Util'); const Collection = require('../util/Collection'); const Constants = require('../util/Constants'); @@ -137,6 +138,21 @@ class Message { */ this.webhookID = data.webhook_id || null; + /** + * Supplimental application information for group activities + * @type {?ClientApplication} + */ + this.application = data.application ? new ClientApplication(this.client, data.application) : null; + + /** + * Group activity + * @type {?Object} + */ + this.activiy = data.activity ? { + partyID: data.activity.party_id, + type: data.activity.type, + } : null; + /** * Whether this message is a hit in a search * @type {?boolean} diff --git a/src/util/Constants.js b/src/util/Constants.js index 0238ed483..c23af6adf 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -103,9 +103,10 @@ const AllowedImageSizes = [ 2048, ]; -function checkImage({ size, format }) { +function makeImageUrl(root, { format = 'webp', size } = {}) { if (format && !AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format); if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size); + return `${root}.${format}${size ? `?size=${size}` : ''}`; } exports.Endpoints = { @@ -116,25 +117,16 @@ exports.Endpoints = { DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, Avatar: (userID, hash, format = 'default', size) => { if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; - checkImage({ size, format }); - return `${root}/avatars/${userID}/${hash}.${format}${size ? `?size=${size}` : ''}`; - }, - Icon: (guildID, hash, format = 'webp', size) => { - checkImage({ size, format }); - return `${root}/icons/${guildID}/${hash}.${format}${size ? `?size=${size}` : ''}`; - }, - AppIcon: (clientID, hash, format = 'webp', size) => { - checkImage({ size, format }); - return `${root}/app-icons/${clientID}/${hash}.${format}${size ? `?size=${size}` : ''}`; - }, - GDMIcon: (channelID, hash, format = 'webp', size) => { - checkImage({ size, format }); - return `${root}/channel-icons/${channelID}/${hash}.${format}${size ? `?size=${size}` : ''}`; - }, - Splash: (guildID, hash, format = 'webp', size) => { - checkImage({ size, format }); - return `${root}/splashes/${guildID}/${hash}.${format}${size ? `?size=${size}` : ''}`; + return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); }, + Icon: (guildID, hash, format = 'webp', size) => + makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }), + AppIcon: (clientID, hash, { format = 'webp', size } = {}) => + makeImageUrl(`${root}/app-icons/${clientID}/${hash}`, { size, format }), + GDMIcon: (channelID, hash, format = 'webp', size) => + makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }), + Splash: (guildID, hash, format = 'webp', size) => + makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }), }; }, invite: (root, code) => `${root}/${code}`, @@ -570,6 +562,11 @@ exports.UserFlags = { HYPESQUAD: 1 << 2, }; +exports.ClientApplicationAssetTypes = { + SMALL: 1, + BIG: 2, +}; + exports.Colors = { DEFAULT: 0x000000, AQUA: 0x1ABC9C, From fbdf028b86c80f9b5a0f5b1f27dc7e96ecfe8eaf Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 12 Aug 2017 01:58:51 -0700 Subject: [PATCH 0199/1359] :3 (#1774) --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 440552f95..ccfe5e098 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -148,7 +148,7 @@ class Message { * Group activity * @type {?Object} */ - this.activiy = data.activity ? { + this.activity = data.activity ? { partyID: data.activity.party_id, type: data.activity.type, } : null; From 8034c0437dacb5bcc39a79ebbe1fbecc145ea42f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 12 Aug 2017 11:55:55 +0200 Subject: [PATCH 0200/1359] Fix VoiceChannel#setName and ChannelData#userLimit is a only voice thing (#1771) --- src/structures/GuildChannel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 71c321c1f..e26c2f614 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -202,7 +202,7 @@ class GuildChannel extends Channel { * @property {number} [position] The position of the channel * @property {string} [topic] The topic of the text channel * @property {number} [bitrate] The bitrate of the voice channel - * @property {number} [userLimit] The user limit of the channel + * @property {number} [userLimit] The user limit of voice the channel */ /** @@ -222,7 +222,7 @@ class GuildChannel extends Channel { name: (data.name || this.name).trim(), topic: data.topic || this.topic, position: data.position || this.position, - bitrate: data.bitrate || this.bitrate, + bitrate: data.bitrate || (this.bitrate ? this.bitrate * 1000 : undefined), user_limit: data.userLimit || this.userLimit, }, reason, From 9c52030c292fef774567201633f4dc53f532c081 Mon Sep 17 00:00:00 2001 From: Johnson Chen Date: Sat, 12 Aug 2017 20:01:43 +1000 Subject: [PATCH 0201/1359] ClientUser Fixes (#1741) * Fixes #1702 * Remove Comments * Follow what Gus said... I hope * JSDoc * Update ClientUser.js * TIL my knowledge about JSDocs was a lie --- src/structures/ClientUser.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index e533e2284..1190feb3c 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -93,19 +93,20 @@ class ClientUser extends User { } } - edit(data, password) { - const _data = {}; - _data.username = data.username || this.username; - _data.avatar = this.client.resolver.resolveBase64(data.avatar); - + edit(data, passcode) { if (!this.bot) { - _data.email = data.email || this.email; - _data.password = password; - if (data.new_password) _data.new_password = data.newPassword; + if (typeof passcode !== 'object') { + data.password = passcode; + } else { + data.code = passcode.mfaCode; + data.password = passcode.password; + } } - return this.client.api.users('@me').patch({ data }) - .then(newData => this.client.actions.UserUpdate.handle(newData).updated); + .then(newData => { + this.client.token = newData.token; + return this.client.actions.UserUpdate.handle(newData).updated; + }); } /** @@ -145,7 +146,10 @@ class ClientUser extends User { * Changes the password for the client user's account. * This is only available when using a user account. * @param {string} newPassword New password to change to - * @param {string} oldPassword Current password + * @param {Object|string} options Object containing an MFA code, password or both. + * Can be just a string for the password. + * @param {string} [options.oldPassword] Current password + * @param {string} [options.mfaCode] Timed MFA Code * @returns {Promise} * @example * // Set password @@ -153,8 +157,8 @@ class ClientUser extends User { * .then(user => console.log('New password set!')) * .catch(console.error); */ - setPassword(newPassword, oldPassword) { - return this.edit({ password: newPassword }, oldPassword); + setPassword(newPassword, options) { + return this.edit({ new_password: newPassword }, { password: options.oldPassword, mfaCode: options.mfaCode }); } /** From b2c7fcb6038a5656a581bdd79d32aff431094ba6 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 13 Aug 2017 13:56:45 +0200 Subject: [PATCH 0202/1359] Add support for new game types (#1777) --- src/structures/Presence.js | 8 +++++--- src/util/Constants.js | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index f08e27eff..2306e9a81 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,3 +1,5 @@ +const Constants = require('../util/Constants'); + /** * Represents a user's presence. */ @@ -53,9 +55,9 @@ class Game { /** * The type of the game status - * @type {number} + * @type {GameType} */ - this.type = data.type; + this.type = Constants.GameTypes[data.type]; /** * If the game is being streamed, a link to the stream @@ -70,7 +72,7 @@ class Game { * @readonly */ get streaming() { - return this.type === 1; + return this.type === Constants.GameTypes[1]; } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index c23af6adf..5c31eb8aa 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -349,6 +349,21 @@ exports.MessageTypes = [ 'GUILD_MEMBER_JOIN', ]; +/** + * The type of a game of a users presence, e.g. `PLAYING`. Here are the available types: + * - PLAYING + * - STREAMING + * - LISTENING + * - WATCHING + * @typedef {string} GameType + */ +exports.GameTypes = [ + 'PLAYING', + 'STREAMING', + 'LISTENING', + 'WATCHING', +]; + exports.ExplicitContentFilterTypes = [ 'DISABLED', 'NON_FRIENDS', From 57977b063ef08416c0449b6b4e2ba1d401c2ed36 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 13 Aug 2017 04:57:25 -0700 Subject: [PATCH 0203/1359] channel stuff (#1775) * channel stuff * abstract channel creation * Update Channel.js * Update Channel.js * Update Channel.js * Update Guild.js * Update Constants.js * e --- src/client/ClientDataManager.js | 25 +++------------------- src/structures/Channel.js | 36 +++++++++++++++++++++++++++++++- src/structures/DMChannel.js | 1 - src/structures/GroupDMChannel.js | 1 - src/structures/Guild.js | 4 +++- src/structures/VoiceChannel.js | 2 -- src/util/Constants.js | 2 +- 7 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/client/ClientDataManager.js b/src/client/ClientDataManager.js index e8c5721eb..d6cc3be20 100644 --- a/src/client/ClientDataManager.js +++ b/src/client/ClientDataManager.js @@ -2,12 +2,9 @@ const Constants = require('../util/Constants'); const Util = require('../util/Util'); const Guild = require('../structures/Guild'); const User = require('../structures/User'); -const DMChannel = require('../structures/DMChannel'); -const Emoji = require('../structures/Emoji'); -const TextChannel = require('../structures/TextChannel'); -const VoiceChannel = require('../structures/VoiceChannel'); +const Channel = require('../structures/Channel'); const GuildChannel = require('../structures/GuildChannel'); -const GroupDMChannel = require('../structures/GroupDMChannel'); +const Emoji = require('../structures/Emoji'); class ClientDataManager { constructor(client) { @@ -47,23 +44,7 @@ class ClientDataManager { newChannel(data, guild) { const already = this.client.channels.has(data.id); - let channel; - if (data.type === Constants.ChannelTypes.DM) { - channel = new DMChannel(this.client, data); - } else if (data.type === Constants.ChannelTypes.GROUP_DM) { - channel = new GroupDMChannel(this.client, data); - } else { - guild = guild || this.client.guilds.get(data.guild_id); - if (guild) { - if (data.type === Constants.ChannelTypes.TEXT) { - channel = new TextChannel(guild, data); - guild.channels.set(channel.id, channel); - } else if (data.type === Constants.ChannelTypes.VOICE) { - channel = new VoiceChannel(guild, data); - guild.channels.set(channel.id, channel); - } - } - } + const channel = Channel.create(this.client, data, guild); if (channel) { if (this.pastReady && !already) this.client.emit(Constants.Events.CHANNEL_CREATE, channel); diff --git a/src/structures/Channel.js b/src/structures/Channel.js index d8f877c9a..d82545abc 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -1,4 +1,5 @@ const Snowflake = require('../util/Snowflake'); +const Constants = require('../util/Constants'); /** * Represents any channel on Discord. @@ -13,15 +14,17 @@ class Channel { */ Object.defineProperty(this, 'client', { value: client }); + const type = Object.keys(Constants.ChannelTypes)[data.type]; /** * The type of the channel, either: * * `dm` - a DM channel * * `group` - a Group DM channel * * `text` - a guild text channel * * `voice` - a guild voice channel + * * `unknown` - a generic channel of unknown type, could be Channel or GuildChannel * @type {string} */ - this.type = null; + this.type = type ? type.toLowerCase() : 'unknown'; if (data) this.setup(data); } @@ -64,6 +67,37 @@ class Channel { delete() { return this.client.api.channels(this.id).delete().then(() => this); } + + static create(client, data, guild) { + const DMChannel = require('./DMChannel'); + const GroupDMChannel = require('./GroupDMChannel'); + const TextChannel = require('./TextChannel'); + const VoiceChannel = require('./VoiceChannel'); + const GuildChannel = require('./GuildChannel'); + const types = Constants.ChannelTypes; + let channel; + if (data.type === types.DM) { + channel = new DMChannel(client, data); + } else if (data.type === types.GROUP) { + channel = new GroupDMChannel(client, data); + } else { + guild = guild || client.guilds.get(data.guild_id); + if (guild) { + switch (data.type) { + case types.TEXT: + channel = new TextChannel(guild, data); + break; + case types.VOICE: + channel = new VoiceChannel(guild, data); + break; + default: + channel = new GuildChannel(guild, data); + } + guild.channels.set(channel.id, channel); + } + } + return channel; + } } module.exports = Channel; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 06701d14f..91d8fca1d 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -10,7 +10,6 @@ const Collection = require('../util/Collection'); class DMChannel extends Channel { constructor(client, data) { super(client, data); - this.type = 'dm'; this.messages = new Collection(); this._typing = new Map(); } diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 0511cf050..bd2f3a7ea 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -33,7 +33,6 @@ const Constants = require('../util/Constants'); class GroupDMChannel extends Channel { constructor(client, data) { super(client, data); - this.type = 'group'; this.messages = new Collection(); this._typing = new Map(); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1b36eb2f4..19b1d4c33 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -960,7 +960,9 @@ class Guild { return this.client.api.guilds(this.id).channels.post({ data: { - name, type, permission_overwrites: overwrites, + name, + type: Constants.ChannelTypes[type.toUpperCase()], + permission_overwrites: overwrites, }, reason, }).then(data => this.client.actions.ChannelCreate.handle(data).channel); diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index f446d945e..2a67c9379 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -15,8 +15,6 @@ class VoiceChannel extends GuildChannel { * @type {Collection} */ this.members = new Collection(); - - this.type = 'voice'; } setup(data) { diff --git a/src/util/Constants.js b/src/util/Constants.js index 5c31eb8aa..44ea09542 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -173,7 +173,7 @@ exports.ChannelTypes = { TEXT: 0, DM: 1, VOICE: 2, - GROUP_DM: 3, + GROUP: 3, }; exports.OPCodes = { From 71f2cc10f19eb0103c41222acfa60bbfacb8eaef Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 15 Aug 2017 12:54:09 -0700 Subject: [PATCH 0204/1359] add cover image (#1780) * add cover image * Update ClientApplication.js --- src/structures/ClientApplication.js | 32 +++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 6e00c654e..adf0356b8 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -2,13 +2,13 @@ const Snowflake = require('../util/Snowflake'); const Constants = require('../util/Constants'); /** - * Represents an OAuth2 Application. + * Represents a Client OAuth2 Application. */ -class OAuth2Application { +class ClientApplication { constructor(client, data) { /** * The client that instantiated the application - * @name OAuth2Application#client + * @name ClientApplication#client * @type {Client} * @readonly */ @@ -42,6 +42,12 @@ class OAuth2Application { */ this.icon = data.icon; + /** + * The app's cover image hash + * @type {?string} + */ + this.cover = data.cover_image; + /** * The app's RPC origins * @type {?string[]} @@ -129,6 +135,20 @@ class OAuth2Application { return Constants.Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.icon, { format, size }); } + /** + * A link to this application's cover image + * @param {Object} [options={}] Options for the cover image url + * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` + * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` + * @returns {?string} URL to the cover image + */ + coverImage({ format, size } = {}) { + if (!this.cover) return null; + return Constants.Endpoints + .CDN(this.client.options.http.cdn) + .AppIcon(this.id, this.cover, { format, size }); + } + /** * Get rich presence assets * @returns {Promise} @@ -165,7 +185,7 @@ class OAuth2Application { */ resetSecret() { return this.client.api.oauth2.applications[this.id].reset.post() - .then(app => new OAuth2Application(this.client, app)); + .then(app => new ClientApplication(this.client, app)); } /** @@ -175,7 +195,7 @@ class OAuth2Application { */ resetToken() { return this.client.api.oauth2.applications[this.id].bot.reset.post() - .then(app => new OAuth2Application(this.client, Object.assign({}, this, { bot: app }))); + .then(app => new ClientApplication(this.client, Object.assign({}, this, { bot: app }))); } /** @@ -187,4 +207,4 @@ class OAuth2Application { } } -module.exports = OAuth2Application; +module.exports = ClientApplication; From 6065fe1f8c9917f0a57a927cdd465de02ac776f3 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 15 Aug 2017 12:55:07 -0700 Subject: [PATCH 0205/1359] update image sizes, adding some more (#1781) --- src/util/Constants.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 44ea09542..5651614c7 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -95,13 +95,7 @@ const AllowedImageFormats = [ 'gif', ]; -const AllowedImageSizes = [ - 128, - 256, - 512, - 1024, - 2048, -]; +const AllowedImageSizes = Array.from({ length: 8 }, (e, i) => 2 ** (i + 4)); function makeImageUrl(root, { format = 'webp', size } = {}) { if (format && !AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format); From e677543c30274216104beaab36fbeb2637e6bf5a Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 17 Aug 2017 18:27:32 +0200 Subject: [PATCH 0206/1359] Allow to set the new game types via ClientUser#setPresence and ClientUser#setGame (#1782) * Allow to set the new game types via ClientUser#setPresence and setGame * Accept string version of types, fix options parameter, remove Presence#streaming * One line if statement, don't reuse data.game when game is already reassigned and fix error message * Removed redundant if statement --- src/structures/ClientUser.js | 19 ++++++++++++++----- src/structures/Presence.js | 9 --------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 1190feb3c..6363d23a9 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -187,6 +187,7 @@ class ClientUser extends User { * @property {boolean} [afk] Whether the user is AFK * @property {Object} [game] Game the user is playing * @property {string} [game.name] Name of the game + * @property {GameType|number} [game.type] Type of the game * @property {string} [game.url] Twitch stream URL */ @@ -211,7 +212,7 @@ class ClientUser extends User { } if (data.status) { - if (typeof data.status !== 'string') throw new TypeError('STATUS_TYPE'); + if (typeof data.status !== 'string') throw new TypeError('INVALID_TYPE', 'status', 'string'); if (this.bot) { status = data.status; } else { @@ -222,7 +223,12 @@ class ClientUser extends User { if (data.game) { game = data.game; - if (game.url) game.type = 1; + if (typeof game.type === 'string') { + game.type = Constants.GameTypes.indexOf(game.type); + if (game.type === -1) throw new TypeError('INVALID_TYPE', 'type', 'GameType'); + } else if (typeof game.type !== 'number') { + game.type = game.url ? 1 : 0; + } } else if (typeof data.game !== 'undefined') { game = null; } @@ -266,15 +272,18 @@ class ClientUser extends User { /** * Sets the game the client user is playing. * @param {?string} game Game being played - * @param {string} [streamingURL] Twitch stream URL + * @param {Object} [options] Options for setting the game + * @param {string} [options.url] Twitch stream URL + * @param {GameType|number} [options.type] Type of the game * @returns {Promise} */ - setGame(game, streamingURL) { + setGame(game, { url, type } = {}) { if (!game) return this.setPresence({ game: null }); return this.setPresence({ game: { name: game, - url: streamingURL, + type, + url, }, }); } diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 2306e9a81..0284260fd 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -66,15 +66,6 @@ class Game { this.url = data.url || null; } - /** - * Whether or not the game is being streamed - * @type {boolean} - * @readonly - */ - get streaming() { - return this.type === Constants.GameTypes[1]; - } - /** * Whether this game is equal to another game * @param {Game} game The game to compare with From 23d42d7e2232149d903a3e49060a66640efdc015 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 17 Aug 2017 09:27:50 -0700 Subject: [PATCH 0207/1359] permissions rename :3 (#1788) * permissions rename :3 * Update Permissions.js * Update TextChannel.js * Update GuildChannel.js --- src/structures/GuildChannel.js | 15 +++++++++++++++ src/structures/TextChannel.js | 15 --------------- src/util/Permissions.js | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index e26c2f614..eb3817c52 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -195,6 +195,21 @@ class GuildChannel extends Channel { .then(() => this); } + /** + * A collection of members that can see this channel, mapped by their ID + * @type {Collection} + * @readonly + */ + get members() { + const members = new Collection(); + for (const member of this.guild.members.values()) { + if (this.permissionsFor(member).has('VIEW_CHANNEL')) { + members.set(member.id, member); + } + } + return members; + } + /** * The data for a guild channel. * @typedef {Object} ChannelData diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 279a16b5c..7a817d089 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -35,21 +35,6 @@ class TextChannel extends GuildChannel { this.lastMessageID = data.last_message_id; } - /** - * A collection of members that can see this channel, mapped by their ID - * @type {Collection} - * @readonly - */ - get members() { - const members = new Collection(); - for (const member of this.guild.members.values()) { - if (this.permissionsFor(member).has('READ_MESSAGES')) { - members.set(member.id, member); - } - } - return members; - } - /** * Fetch all webhooks for the channel. * @returns {Promise>} diff --git a/src/util/Permissions.js b/src/util/Permissions.js index b6dd20478..679e3c6e1 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -110,7 +110,7 @@ class Permissions { * - `MANAGE_GUILD` (edit the guild information, region, etc.) * - `ADD_REACTIONS` (add new reactions to messages) * - `VIEW_AUDIT_LOG` - * - `READ_MESSAGES` + * - `VIEW_CHANNELS` * - `SEND_MESSAGES` * - `SEND_TTS_MESSAGES` * - `MANAGE_MESSAGES` (delete messages and reactions) @@ -143,7 +143,7 @@ Permissions.FLAGS = { ADD_REACTIONS: 1 << 6, VIEW_AUDIT_LOG: 1 << 7, - READ_MESSAGES: 1 << 10, + VIEW_CHANNEL: 1 << 10, SEND_MESSAGES: 1 << 11, SEND_TTS_MESSAGES: 1 << 12, MANAGE_MESSAGES: 1 << 13, From 5ce0def9d087464a367ba3bb0d0a6b33c88124d5 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 17 Aug 2017 09:28:01 -0700 Subject: [PATCH 0208/1359] fix sync (#1792) * Update Ready.js * Update ClientDataManager.js --- src/client/ClientDataManager.js | 1 + src/client/websocket/packets/handlers/Ready.js | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/ClientDataManager.js b/src/client/ClientDataManager.js index d6cc3be20..714fc30d4 100644 --- a/src/client/ClientDataManager.js +++ b/src/client/ClientDataManager.js @@ -19,6 +19,7 @@ class ClientDataManager { const already = this.client.guilds.has(data.id); const guild = new Guild(this.client, data); this.client.guilds.set(guild.id, guild); + if (!this.client.user.bot && this.client.options.sync) this.client.syncGuilds([guild]); if (this.pastReady && !already) { /** * Emitted whenever the client joins a guild. diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index 8c2492abf..29745c42c 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -44,8 +44,6 @@ class ReadyHandler extends AbstractHandler { } } - if (!client.user.bot && client.options.sync) client.setInterval(client.syncGuilds.bind(client), 30000); - if (!client.users.has('1')) { client.dataManager.newUser({ id: '1', From 2aa2f73c74582652f3304da861c6d496255dd499 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Thu, 17 Aug 2017 20:04:01 +0200 Subject: [PATCH 0209/1359] Docs cleanup --- docs/examples/greeting.js | 6 +- src/client/ClientDataResolver.js | 6 +- src/client/voice/VoiceBroadcast.js | 24 +- src/client/voice/VoiceConnection.js | 28 +-- .../voice/dispatcher/StreamDispatcher.js | 10 +- src/client/voice/opus/BaseOpusEngine.js | 8 +- src/client/voice/player/AudioPlayer.js | 2 +- src/client/voice/receiver/VoiceReceiver.js | 7 +- src/client/websocket/WebSocketConnection.js | 8 +- src/client/websocket/WebSocketManager.js | 6 +- src/errors/DJSError.js | 6 +- src/sharding/Shard.js | 8 +- src/sharding/ShardClientUtil.js | 8 +- src/sharding/ShardingManager.js | 8 +- src/structures/Attachment.js | 2 +- src/structures/Channel.js | 4 +- src/structures/ClientApplication.js | 8 +- src/structures/ClientUser.js | 26 +-- src/structures/Emoji.js | 6 +- src/structures/GroupDMChannel.js | 2 +- src/structures/Guild.js | 64 +++--- src/structures/GuildAuditLogs.js | 2 +- src/structures/GuildChannel.js | 26 +-- src/structures/GuildMember.js | 2 +- src/structures/Message.js | 28 +-- src/structures/MessageCollector.js | 9 +- src/structures/MessageEmbed.js | 44 ++-- src/structures/MessageMentions.js | 4 +- src/structures/Presence.js | 2 +- src/structures/ReactionCollector.js | 6 +- src/structures/Role.js | 34 +-- src/structures/TextChannel.js | 6 +- src/structures/User.js | 5 +- src/structures/UserProfile.js | 2 +- src/structures/VoiceChannel.js | 12 +- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 16 +- src/util/Constants.js | 208 +++++++++--------- src/util/Permissions.js | 62 +++--- src/util/Util.js | 6 +- 40 files changed, 366 insertions(+), 359 deletions(-) diff --git a/docs/examples/greeting.js b/docs/examples/greeting.js index e9c92e076..55bf2d00d 100644 --- a/docs/examples/greeting.js +++ b/docs/examples/greeting.js @@ -19,11 +19,7 @@ client.on('ready', () => { // Create an event listener for new guild members client.on('guildMemberAdd', member => { - // Send the message to the guilds default channel (usually #general), mentioning the member - member.guild.defaultChannel.send(`Welcome to the server, ${member}!`); - - // If you want to send the message to a designated channel on a server instead - // you can do the following: + // Send the message to a designated channel on a server: const channel = member.guild.channels.find('name', 'member-log'); // Do nothing if the channel wasn't found on this server if (!channel) return; diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index ba7df252a..1cc447708 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -29,7 +29,7 @@ class ClientDataResolver { /** * Data that resolves to give a User object. This can be: * * A User object - * * A user ID + * * A Snowflake * * A Message object (resolves to the message author) * * A Guild object (owner of the guild) * * A GuildMember object @@ -66,7 +66,7 @@ class ClientDataResolver { /** * Data that resolves to give a Guild object. This can be: * * A Guild object - * * A Guild ID + * * A Snowflake * @typedef {Guild|Snowflake} GuildResolvable */ @@ -128,7 +128,7 @@ class ClientDataResolver { * * A Channel object * * A Message object (the channel the message was sent in) * * A Guild object (the #general channel) - * * A channel ID + * * A Snowflake * @typedef {Channel|Guild|Message|Snowflake} ChannelResolvable */ diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index beb57fd06..837dc3925 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -18,7 +18,7 @@ const ffmpegArguments = [ * ```js * const broadcast = client.createVoiceBroadcast(); * broadcast.playFile('./music.mp3'); - * // play "music.mp3" in all voice connections that the client is in + * // Play "music.mp3" in all voice connections that the client is in * for (const connection of client.voiceConnections.values()) { * connection.playBroadcast(broadcast); * } @@ -141,12 +141,12 @@ class VoiceBroadcast extends VolumeInterface { * const broadcast = client.createVoiceBroadcast(); * * voiceChannel.join() - * .then(connection => { - * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); - * broadcast.playStream(stream); - * const dispatcher = connection.playBroadcast(broadcast); - * }) - * .catch(console.error); + * .then(connection => { + * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); + * broadcast.playStream(stream); + * const dispatcher = connection.playBroadcast(broadcast); + * }) + * .catch(console.error); */ playStream(stream, options = {}) { this.setVolume(options.volume || 1); @@ -163,11 +163,11 @@ class VoiceBroadcast extends VolumeInterface { * const broadcast = client.createVoiceBroadcast(); * * voiceChannel.join() - * .then(connection => { - * broadcast.playFile('C:/Users/Discord/Desktop/music.mp3'); - * const dispatcher = connection.playBroadcast(broadcast); - * }) - * .catch(console.error); + * .then(connection => { + * broadcast.playFile('C:/Users/Discord/Desktop/music.mp3'); + * const dispatcher = connection.playBroadcast(broadcast); + * }) + * .catch(console.error); */ playFile(file, options = {}) { this.setVolume(options.volume || 1); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 18c5ee084..16bf14004 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -12,9 +12,10 @@ const { Error } = require('../../errors'); * Represents a connection to a guild's voice server. * ```js * // Obtained using: - * voiceChannel.join().then(connection => { + * voiceChannel.join() + * .then(connection => { * - * }); + * }); * ``` * @extends {EventEmitter} */ @@ -311,7 +312,7 @@ class VoiceConnection extends EventEmitter { } /** - * Internally disconnects (doesn't send disconnect packet.) + * Internally disconnects (doesn't send disconnect packet). * @private */ _disconnect() { @@ -454,10 +455,10 @@ class VoiceConnection extends EventEmitter { * @example * // Play files natively * voiceChannel.join() - * .then(connection => { - * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); - * }) - * .catch(console.error); + * .then(connection => { + * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); + * }) + * .catch(console.error); */ playFile(file, options) { return this.player.playUnknownStream(`file:${file}`, options); @@ -483,11 +484,11 @@ class VoiceConnection extends EventEmitter { * const ytdl = require('ytdl-core'); * const streamOptions = { seek: 0, volume: 1 }; * voiceChannel.join() - * .then(connection => { - * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); - * const dispatcher = connection.playStream(stream, streamOptions); - * }) - * .catch(console.error); + * .then(connection => { + * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); + * const dispatcher = connection.playStream(stream, streamOptions); + * }) + * .catch(console.error); */ playStream(stream, options) { return this.player.playUnknownStream(stream, options); @@ -531,7 +532,8 @@ class VoiceConnection extends EventEmitter { } /** - * Creates a VoiceReceiver so you can start listening to voice data. It's recommended to only create one of these. + * Creates a VoiceReceiver so you can start listening to voice data. + * It's recommended to only create one of these. * @returns {VoiceReceiver} */ createReceiver() { diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 24df9d069..beef7b9f4 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -89,12 +89,12 @@ class StreamDispatcher extends VolumeInterface { } /** - * Stops sending voice packets to the voice connection (stream may still progress however) + * Stops sending voice packets to the voice connection (stream may still progress however). */ pause() { this.setPaused(true); } /** - * Resumes sending voice packets to the voice connection (may be further on in the stream than when paused) + * Resumes sending voice packets to the voice connection (may be further on in the stream than when paused). */ resume() { this.setPaused(false); } @@ -122,7 +122,7 @@ class StreamDispatcher extends VolumeInterface { /** * Set the bitrate of the current Opus encoder. - * @param {number} bitrate New bitrate, in kbps. + * @param {number} bitrate New bitrate, in kbps * If set to 'auto', the voice channel's bitrate will be used */ setBitrate(bitrate) { @@ -140,7 +140,7 @@ class StreamDispatcher extends VolumeInterface { /** * Emitted whenever the dispatcher has debug information. * @event StreamDispatcher#debug - * @param {string} info the debug info + * @param {string} info The debug info */ this.setSpeaking(true); while (repeats--) { @@ -294,7 +294,7 @@ class StreamDispatcher extends VolumeInterface { this.emit(type, reason); /** * Emitted once the dispatcher ends. - * @param {string} [reason] the reason the dispatcher ended + * @param {string} [reason] The reason the dispatcher ended * @event StreamDispatcher#end */ if (type !== 'end') this.emit('end', `destroyed due to ${type} - ${reason}`); diff --git a/src/client/voice/opus/BaseOpusEngine.js b/src/client/voice/opus/BaseOpusEngine.js index 40c6204fd..a51044905 100644 --- a/src/client/voice/opus/BaseOpusEngine.js +++ b/src/client/voice/opus/BaseOpusEngine.js @@ -4,10 +4,10 @@ */ class BaseOpus { /** - * @param {Object} [options] The options to apply to the Opus engine. - * @param {number} [options.bitrate=48] The desired bitrate (kbps). - * @param {boolean} [options.fec=false] Whether to enable forward error correction. - * @param {number} [options.plp=0] The expected packet loss percentage. + * @param {Object} [options] The options to apply to the Opus engine + * @param {number} [options.bitrate=48] The desired bitrate (kbps) + * @param {boolean} [options.fec=false] Whether to enable forward error correction + * @param {number} [options.plp=0] The expected packet loss percentage */ constructor({ bitrate = 48, fec = false, plp = 0 } = {}) { this.ctl = { diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index ca0f759ff..4abcc7d8b 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -83,7 +83,7 @@ class AudioPlayer extends EventEmitter { /** * Set the bitrate of the current Opus encoder. - * @param {number} value New bitrate, in kbps. + * @param {number} value New bitrate, in kbps * If set to 'auto', the voice channel's bitrate will be used */ setBitrate(value) { diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index 6f2f5722b..025a94054 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -11,9 +11,10 @@ nonce.fill(0); * Receives voice data from a voice connection. * ```js * // Obtained using: - * voiceChannel.join().then(connection => { - * const receiver = connection.createReceiver(); - * }); + * voiceChannel.join() + * .then(connection => { + * const receiver = connection.createReceiver(); + * }); * ``` * @extends {EventEmitter} */ diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index ab6e354c6..6f977f395 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -29,12 +29,12 @@ const WebSocket = (function findWebSocket() { class WebSocketConnection extends EventEmitter { /** * @param {WebSocketManager} manager The WebSocket manager - * @param {string} gateway WebSocket gateway to connect to + * @param {string} gateway The WebSocket gateway to connect to */ constructor(manager, gateway) { super(); /** - * WebSocket Manager of this connection + * The WebSocket Manager of this connection * @type {WebSocketManager} */ this.manager = manager; @@ -233,7 +233,7 @@ class WebSocketConnection extends EventEmitter { /** * Creates a connection to a gateway. - * @param {string} gateway Gateway to connect to + * @param {string} gateway The gateway to connect to * @param {number} [after=0] How long to wait before connecting * @param {boolean} [force=false] Whether or not to force a new connection even if one already exists * @returns {boolean} @@ -358,7 +358,7 @@ class WebSocketConnection extends EventEmitter { /** * Called whenever an error occurs with the WebSocket. - * @param {Error} error Error that occurred + * @param {Error} error The error that occurred */ onError(error) { if (error && error.message === 'uWs client connection error') { diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 8f120b975..fe0924a27 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -3,7 +3,7 @@ const Constants = require('../../util/Constants'); const WebSocketConnection = require('./WebSocketConnection'); /** - * WebSocket Manager of the client + * WebSocket Manager of the client. * @private */ class WebSocketManager extends EventEmitter { @@ -23,7 +23,7 @@ class WebSocketManager extends EventEmitter { } /** - * Sends a heartbeat on the available connection + * Sends a heartbeat on the available connection. * @returns {void} */ heartbeat() { @@ -67,7 +67,7 @@ class WebSocketManager extends EventEmitter { /** * Connects the client to a gateway. - * @param {string} gateway Gateway to connect to + * @param {string} gateway The gateway to connect to * @returns {boolean} */ connect(gateway) { diff --git a/src/errors/DJSError.js b/src/errors/DJSError.js index b922b711d..55b549902 100644 --- a/src/errors/DJSError.js +++ b/src/errors/DJSError.js @@ -6,7 +6,7 @@ const assert = require('assert'); const util = require('util'); /** - * Extend an error of some sort into a DiscordjsError + * Extend an error of some sort into a DiscordjsError. * @param {Error} Base Base error to extend * @returns {DiscordjsError} */ @@ -29,7 +29,7 @@ function makeDiscordjsError(Base) { } /** - * Format the message for an error + * Format the message for an error. * @param {string} key Error key * @param {Array<*>} args Arguments to pass for util format or as function args * @returns {string} Formatted string @@ -49,7 +49,7 @@ function message(key, args) { } /** - * Register an error code and message + * Register an error code and message. * @param {string} sym Unique name for the error * @param {*} val Value of the error */ diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index f0cc51098..e67bece78 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -70,9 +70,11 @@ class Shard { * @param {string} prop Name of the client property to get, using periods for nesting * @returns {Promise<*>} * @example - * shard.fetchClientValue('guilds.size').then(count => { - * console.log(`${count} guilds in shard ${shard.id}`); - * }).catch(console.error); + * shard.fetchClientValue('guilds.size') + * .then(count => { + * console.log(`${count} guilds in shard ${shard.id}`); + * }) + * .catch(console.error); */ fetchClientValue(prop) { if (this._fetches.has(prop)) return this._fetches.get(prop); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 549353df2..b6453625c 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -50,9 +50,11 @@ class ShardClientUtil { * @param {string} prop Name of the client property to get, using periods for nesting * @returns {Promise} * @example - * client.shard.fetchClientValues('guilds.size').then(results => { - * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`); - * }).catch(console.error); + * client.shard.fetchClientValues('guilds.size') + * .then(results => { + * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`); + * }) + * .catch(console.error); */ fetchClientValues(prop) { return new Promise((resolve, reject) => { diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 8c16096e7..5c9632890 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -181,9 +181,11 @@ class ShardingManager extends EventEmitter { * @param {string} prop Name of the client property to get, using periods for nesting * @returns {Promise} * @example - * manager.fetchClientValues('guilds.size').then(results => { - * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`); - * }).catch(console.error); + * manager.fetchClientValues('guilds.size') + * .then(results => { + * console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`); + * }) + * .catch(console.error); */ fetchClientValues(prop) { if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS')); diff --git a/src/structures/Attachment.js b/src/structures/Attachment.js index 94357683a..fd4fa8f80 100644 --- a/src/structures/Attachment.js +++ b/src/structures/Attachment.js @@ -1,5 +1,5 @@ /** - * Represents an attachment in a message + * Represents an attachment in a message. */ class Attachment { constructor(file, name) { diff --git a/src/structures/Channel.js b/src/structures/Channel.js index d82545abc..acf94e298 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -61,8 +61,8 @@ class Channel { * @example * // Delete the channel * channel.delete() - * .then() // Success - * .catch(console.error); // Log error + * .then() // Success + * .catch(console.error); // Log error */ delete() { return this.client.api.channels(this.id).delete().then(() => this); diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index adf0356b8..df0d310e8 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -124,7 +124,7 @@ class ClientApplication { } /** - * A link to the application's icon + * A link to the application's icon. * @param {Object} [options={}] Options for the icon url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` @@ -136,7 +136,7 @@ class ClientApplication { } /** - * A link to this application's cover image + * A link to this application's cover image. * @param {Object} [options={}] Options for the cover image url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` @@ -150,7 +150,7 @@ class ClientApplication { } /** - * Get rich presence assets + * Get rich presence assets. * @returns {Promise} */ fetchAssets() { @@ -163,7 +163,7 @@ class ClientApplication { } /** - * Create a rich presence asset + * Create a rich presence asset. * @param {string} name Name of the asset * @param {Base64Resolvable} data Data of the asset * @param {string} type Type of the asset. `big`, or `small` diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 6363d23a9..79d448f84 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -83,7 +83,7 @@ class ClientUser extends User { /** * All of the user's guild settings * @type {Collection} - * This is only filled when using a user account + * This is only filled when using a user account. */ this.guildSettings = new Collection(); if (data.user_guild_settings) { @@ -119,8 +119,8 @@ class ClientUser extends User { * @example * // Set username * client.user.setUsername('discordjs') - * .then(user => console.log(`My new username is ${user.username}`)) - * .catch(console.error); + * .then(user => console.log(`My new username is ${user.username}`)) + * .catch(console.error); */ setUsername(username, password) { return this.edit({ username }, password); @@ -135,8 +135,8 @@ class ClientUser extends User { * @example * // Set email * client.user.setEmail('bob@gmail.com', 'some amazing password 123') - * .then(user => console.log(`My new email is ${user.email}`)) - * .catch(console.error); + * .then(user => console.log(`My new email is ${user.email}`)) + * .catch(console.error); */ setEmail(email, password) { return this.edit({ email }, password); @@ -154,8 +154,8 @@ class ClientUser extends User { * @example * // Set password * client.user.setPassword('some new amazing password 456', 'some amazing password 123') - * .then(user => console.log('New password set!')) - * .catch(console.error); + * .then(user => console.log('New password set!')) + * .catch(console.error); */ setPassword(newPassword, options) { return this.edit({ new_password: newPassword }, { password: options.oldPassword, mfaCode: options.mfaCode }); @@ -168,8 +168,8 @@ class ClientUser extends User { * @example * // Set avatar * client.user.setAvatar('./avatar.png') - * .then(user => console.log(`New avatar set!`)) - * .catch(console.error); + * .then(user => console.log(`New avatar set!`)) + * .catch(console.error); */ setAvatar(avatar) { if (typeof avatar === 'string' && avatar.startsWith('data:')) { @@ -253,10 +253,10 @@ class ClientUser extends User { /** * A user's status. Must be one of: - * - `online` - * - `idle` - * - `invisible` - * - `dnd` (do not disturb) + * * `online` + * * `idle` + * * `invisible` + * * `dnd` (do not disturb) * @typedef {string} PresenceStatus */ diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 1920d6fdc..a37c0162a 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -115,10 +115,10 @@ class Emoji { * @param {string} [reason] Reason for editing this emoji * @returns {Promise} * @example - * // Edit a emoji + * // Edit an emoji * emoji.edit({name: 'newemoji'}) - * .then(e => console.log(`Edited emoji ${e}`)) - * .catch(console.error); + * .then(e => console.log(`Edited emoji ${e}`)) + * .catch(console.error); */ edit(data, reason) { return this.client.api.guilds(this.guild.id).emojis(this.id) diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index bd2f3a7ea..78e22f731 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -106,7 +106,7 @@ class GroupDMChannel extends Channel { } /** - * Gets the URL to this Group DM's icon + * Gets the URL to this Group DM's icon. * @param {Object} [options={}] Options for the icon url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 19b1d4c33..f3e2e6257 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -220,7 +220,8 @@ class Guild { if (!this.emojis) { /** - * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji. + * A collection of emojis that are in this guild + * The key is the emoji's ID, the value is the emoji * @type {Collection} */ this.emojis = new Collection(); @@ -261,7 +262,7 @@ class Guild { } /** - * Gets the URL to this guild's icon + * The URL to this guild's icon. * @param {Object} [options={}] Options for the icon url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` @@ -273,7 +274,7 @@ class Guild { } /** - * Gets the acronym that shows up in place of a guild icon + * The acronym that shows up in place of a guild icon. * @type {string} * @readonly */ @@ -282,7 +283,7 @@ class Guild { } /** - * The URL to this guild's splash + * The URL to this guild's splash. * @param {Object} [options={}] Options for the splash url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` * @param {number} [options.size=128] One of `128`, '256', `512`, `1024`, `2048` @@ -443,7 +444,8 @@ class Guild { } /** - * Fetch a collection of invites to this guild. Resolves with a collection mapping invites by their codes. + * Fetch a collection of invites to this guild. + * Resolves with a collection mapping invites by their codes. * @returns {Promise>} */ fetchInvites() { @@ -566,7 +568,7 @@ class Guild { fetchMembers({ query = '', limit = 0 } = {}) { return new Promise((resolve, reject) => { if (this.memberCount === this.members.size) { - resolve((query || limit) ? new Collection() : this.members); + resolve(query || limit ? new Collection() : this.members); return; } this.client.ws.send({ @@ -585,7 +587,7 @@ class Guild { } if (this.memberCount === this.members.size || ((query || limit) && members.size < 1000)) { this.client.removeListener(Constants.Events.GUILD_MEMBERS_CHUNK, handler); - resolve((query || limit) ? fetchedMembers : this.members); + resolve(query || limit ? fetchedMembers : this.members); } }; this.client.on(Constants.Events.GUILD_MEMBERS_CHUNK, handler); @@ -636,11 +638,11 @@ class Guild { * @example * // Set the guild name and region * guild.edit({ - * name: 'Discord Guild', - * region: 'london', + * name: 'Discord Guild', + * region: 'london', * }) - * .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`)) - * .catch(console.error); + * .then(updated => console.log(`New guild name ${updated.name} in region ${updated.region}`)) + * .catch(console.error); */ edit(data, reason) { const _data = {}; @@ -841,8 +843,8 @@ class Guild { * @example * // Ban a user by ID (or with a user/guild member object) * guild.ban('some user ID') - * .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`)) - * .catch(console.error); + * .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`)) + * .catch(console.error); */ ban(user, options = { days: 0 }) { if (options.days) options['delete-message-days'] = options.days; @@ -868,8 +870,8 @@ class Guild { * @example * // Unban a user by ID (or with a user/guild member object) * guild.unban('some user ID') - * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) - * .catch(console.error); + * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) + * .catch(console.error); */ unban(user, reason) { const id = this.client.resolver.resolveUserID(user); @@ -929,8 +931,8 @@ class Guild { * @example * // Create a new text channel * guild.createChannel('new-general', 'text') - * .then(channel => console.log(`Created new channel ${channel}`)) - * .catch(console.error); + * .then(channel => console.log(`Created new channel ${channel}`)) + * .catch(console.error); */ createChannel(name, type, { overwrites, reason } = {}) { if (overwrites instanceof Collection || overwrites instanceof Array) { @@ -981,8 +983,8 @@ class Guild { * @returns {Promise} * @example * guild.updateChannels([{ channel: channelID, position: newChannelIndex }]) - * .then(guild => console.log(`Updated channel positions for ${guild.id}`)) - * .catch(console.error); + * .then(guild => console.log(`Updated channel positions for ${guild.id}`)) + * .catch(console.error); */ setChannelPositions(channelPositions) { const data = new Array(channelPositions.length); @@ -1013,8 +1015,8 @@ class Guild { * @example * // Create a new role * guild.createRole() - * .then(role => console.log(`Created role ${role}`)) - * .catch(console.error); + * .then(role => console.log(`Created role ${role}`)) + * .catch(console.error); * @example * // Create a new role with data and a reason * guild.createRole({ @@ -1024,8 +1026,8 @@ class Guild { * }, * reason: 'we needed a role for Super Cool People', * }) - * .then(role => console.log(`Created role ${role}`)) - * .catch(console.error) + * .then(role => console.log(`Created role ${role}`)) + * .catch(console.error) */ createRole({ data = {}, reason } = {}) { if (data.color) data.color = Util.resolveColor(data.color); @@ -1050,13 +1052,13 @@ class Guild { * @example * // Create a new emoji from a url * guild.createEmoji('https://i.imgur.com/w3duR07.png', 'rip') - * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) - * .catch(console.error); + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); * @example * // Create a new emoji from a file on your computer * guild.createEmoji('./memes/banana.png', 'banana') - * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) - * .catch(console.error); + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); */ createEmoji(attachment, name, { roles, reason } = {}) { if (typeof attachment === 'string' && attachment.startsWith('data:')) { @@ -1102,8 +1104,8 @@ class Guild { * @example * // Leave a guild * guild.leave() - * .then(g => console.log(`Left the guild ${g}`)) - * .catch(console.error); + * .then(g => console.log(`Left the guild ${g}`)) + * .catch(console.error); */ leave() { if (this.ownerID === this.client.user.id) return Promise.reject(new Error('GUILD_OWNED')); @@ -1117,8 +1119,8 @@ class Guild { * @example * // Delete a guild * guild.delete() - * .then(g => console.log(`Deleted the guild ${g}`)) - * .catch(console.error); + * .then(g => console.log(`Deleted the guild ${g}`)) + * .catch(console.error); */ delete() { return this.client.api.guilds(this.id).delete() diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 64ec9a611..616b3abf2 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -184,7 +184,7 @@ class GuildAuditLogsEntry { this.executor = guild.client.users.get(data.user_id); /** - * An entry in the audit log representing a specific change + * An entry in the audit log representing a specific change. * @typedef {object} AuditLogChange * @property {string} key The property that was changed, e.g. `nick` for nickname changes * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index eb3817c52..5e2c4628e 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -146,10 +146,10 @@ class GuildChannel extends Channel { * @example * // Overwrite permissions for a message author * message.channel.overwritePermissions(message.author, { - * SEND_MESSAGES: false + * SEND_MESSAGES: false * }) - * .then(() => console.log('Done!')) - * .catch(console.error); + * .then(() => console.log('Done!')) + * .catch(console.error); */ overwritePermissions(userOrRole, options, reason) { const payload = { @@ -228,8 +228,8 @@ class GuildChannel extends Channel { * @example * // Edit a channel * channel.edit({name: 'new-channel'}) - * .then(c => console.log(`Edited channel ${c}`)) - * .catch(console.error); + * .then(c => console.log(`Edited channel ${c}`)) + * .catch(console.error); */ edit(data, reason) { return this.client.api.channels(this.id).patch({ @@ -252,8 +252,8 @@ class GuildChannel extends Channel { * @example * // Set a new channel name * channel.setName('not_general') - * .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`)) - * .catch(console.error); + * .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`)) + * .catch(console.error); */ setName(name, reason) { return this.edit({ name }, reason); @@ -267,8 +267,8 @@ class GuildChannel extends Channel { * @example * // Set a new channel position * channel.setPosition(2) - * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) - * .catch(console.error); + * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) + * .catch(console.error); */ setPosition(position, relative) { return this.guild.setChannelPosition(this, position, relative).then(() => this); @@ -282,8 +282,8 @@ class GuildChannel extends Channel { * @example * // Set a new channel topic * channel.setTopic('needs more rate limiting') - * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) - * .catch(console.error); + * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) + * .catch(console.error); */ setTopic(topic, reason) { return this.edit({ topic }, reason); @@ -366,8 +366,8 @@ class GuildChannel extends Channel { * @example * // Delete the channel * channel.delete('making room for new channels') - * .then() // Success - * .catch(console.error); // Log error + * .then() // Success + * .catch(console.error); // Log error */ delete(reason) { return this.client.api.channels(this.id).delete({ reason }).then(() => this); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index b91930e51..e4920402f 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -503,7 +503,7 @@ class GuildMember { } /** - * Ban this guild member + * Ban this guild member. * @param {Object|number|string} [options] Ban options. If a number, the number of days to delete messages for, if a * string, the ban reason. Supplying an object allows you to do both. * @param {number} [options.days=0] Number of days of messages to delete diff --git a/src/structures/Message.js b/src/structures/Message.js index ccfe5e098..eb734e819 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -59,8 +59,8 @@ class Message { this.author = this.client.dataManager.newUser(data.author); /** - * Represents the author of the message as a guild member. Only available if the message comes from a guild - * where the author is still a member. + * Represents the author of the message as a guild member + * Only available if the message comes from a guild where the author is still a member * @type {?GuildMember} */ this.member = this.guild ? this.guild.member(this.author) || null : null; @@ -226,8 +226,8 @@ class Message { } /** - * The message contents with all mentions replaced by the equivalent text. If mentions cannot be resolved to a name, - * the relevant mention in the message content will not be converted + * The message contents with all mentions replaced by the equivalent text. + * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted. * @type {string} * @readonly */ @@ -271,8 +271,8 @@ class Message { * @example * // Create a reaction collector * const collector = message.createReactionCollector( - * (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID', - * { time: 15000 } + * (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someID', + * { time: 15000 } * ); * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); @@ -288,8 +288,8 @@ class Message { */ /** - * Similar to createCollector but in promise form. Resolves with a collection of reactions that pass the specified - * filter. + * Similar to createCollector but in promise form. + * Resolves with a collection of reactions that pass the specified filter. * @param {CollectorFilter} filter The filter function to use * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector * @returns {Promise>} @@ -362,8 +362,8 @@ class Message { * @example * // Update the content of a message * message.edit('This is my new content!') - * .then(msg => console.log(`Updated the content of a message from ${msg.author}`)) - * .catch(console.error); + * .then(msg => console.log(`Updated the content of a message from ${msg.author}`)) + * .catch(console.error); */ edit(content, options) { if (!options && typeof content === 'object' && !(content instanceof Array)) { @@ -448,8 +448,8 @@ class Message { * @example * // Delete a message * message.delete() - * .then(msg => console.log(`Deleted message from ${msg.author}`)) - * .catch(console.error); + * .then(msg => console.log(`Deleted message from ${msg.author}`)) + * .catch(console.error); */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { @@ -477,8 +477,8 @@ class Message { * @example * // Reply to a message * message.reply('Hey, I\'m a reply!') - * .then(msg => console.log(`Sent a reply to ${msg.author}`)) - * .catch(console.error); + * .then(msg => console.log(`Sent a reply to ${msg.author}`)) + * .catch(console.error); */ reply(content, options) { if (!options && typeof content === 'object' && !(content instanceof Array)) { diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 8a049f2db..b78b66103 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -21,7 +21,8 @@ class MessageCollector extends Collector { super(channel.client, filter, options); /** - * @type {TextBasedChannel} channel The channel + * The channel + * @type {TextBasedChannel} */ this.channel = channel; @@ -49,7 +50,7 @@ class MessageCollector extends Collector { /** * Handle a message for possible collection. * @param {Message} message The message that could be collected - * @returns {?{key: Snowflake, value: Message}} Message data to collect + * @returns {?{key: Snowflake, value: Message}} * @private */ collect(message) { @@ -64,7 +65,7 @@ class MessageCollector extends Collector { /** * Handle a message for possible disposal. * @param {Message} message The message that could be disposed - * @returns {?string} The message ID. + * @returns {?string} */ dispose(message) { return message.channel.id === this.channel.id ? message.id : null; @@ -72,7 +73,7 @@ class MessageCollector extends Collector { /** * Check after un/collection to see if the collector is done. - * @returns {?string} Reason to end the collector, if any + * @returns {?string} * @private */ endReason() { diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 5f93548b7..6febf9491 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -57,7 +57,7 @@ class MessageEmbed { this.fields = data.fields || []; /** - * The thumbnail of this embed, if there is one + * The thumbnail of this embed (if there is one) * @type {?Object} * @property {string} url URL for this thumbnail * @property {string} proxyURL ProxyURL for this thumbnail @@ -87,7 +87,7 @@ class MessageEmbed { } : null; /** - * The video of this embed, if there is one + * The video of this embed (if there is one) * @type {?Object} * @property {string} url URL of this video * @property {number} height Height of this video @@ -96,7 +96,7 @@ class MessageEmbed { this.video = data.video; /** - * The author of this embed, if there is one + * The author of this embed (if there is one) * @type {?Object} * @property {string} name The name of this author * @property {string} url URL of this author @@ -111,7 +111,7 @@ class MessageEmbed { } : null; /** - * The provider of this embed, if there is one + * The provider of this embed (if there is one) * @type {?Object} * @property {string} name The name of this provider * @property {string} url URL of this provider @@ -132,14 +132,12 @@ class MessageEmbed { } : null; /** - * The files of this embed - * @type {?Object} - * @property {Array} files Files to attach - */ + * The files of this embed + * @type {?Object} + * @property {Array} files Files to attach + */ if (data.files) { - for (let file of data.files) { - if (file instanceof Attachment) file = file.file; - } + for (let file of data.files) if (file instanceof Attachment) file = file.file; } else { data.files = null; } } @@ -166,7 +164,7 @@ class MessageEmbed { * @param {StringResolvable} name The name of the field * @param {StringResolvable} value The value of the field * @param {boolean} [inline=false] Set the field to display inline - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ addField(name, value, inline = false) { if (this.fields.length >= 25) throw new RangeError('EMBED_FIELD_COUNT'); @@ -181,7 +179,7 @@ class MessageEmbed { /** * Convenience function for `.addField('\u200B', '\u200B', inline)`. * @param {boolean} [inline=false] Set the field to display inline - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ addBlankField(inline = false) { return this.addField('\u200B', '\u200B', inline); @@ -191,7 +189,7 @@ class MessageEmbed { * Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when * setting an embed image or author/footer icons. Only one file may be attached. * @param {Array} files Files to attach - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ attachFiles(files) { if (this.files) this.files = this.files.concat(files); @@ -207,7 +205,7 @@ class MessageEmbed { * @param {StringResolvable} name The name of the author * @param {string} [iconURL] The icon URL of the author * @param {string} [url] The URL of the author - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setAuthor(name, iconURL, url) { this.author = { name: Util.resolveString(name), iconURL, url }; @@ -217,7 +215,7 @@ class MessageEmbed { /** * Sets the color of this embed. * @param {ColorResolvable} color The color of the embed - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setColor(color) { this.color = Util.resolveColor(color); @@ -227,7 +225,7 @@ class MessageEmbed { /** * Sets the description of this embed. * @param {StringResolvable} description The description - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setDescription(description) { description = Util.resolveString(description); @@ -240,7 +238,7 @@ class MessageEmbed { * Sets the footer of this embed. * @param {StringResolvable} text The text of the footer * @param {string} [iconURL] The icon URL of the footer - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setFooter(text, iconURL) { text = Util.resolveString(text); @@ -252,7 +250,7 @@ class MessageEmbed { /** * Set the image of this embed. * @param {string} url The URL of the image - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setImage(url) { this.image = { url }; @@ -262,7 +260,7 @@ class MessageEmbed { /** * Set the thumbnail of this embed. * @param {string} url The URL of the thumbnail - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setThumbnail(url) { this.thumbnail = { url }; @@ -272,7 +270,7 @@ class MessageEmbed { /** * Sets the timestamp of this embed. * @param {Date} [timestamp=current date] The timestamp - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setTimestamp(timestamp = new Date()) { this.timestamp = timestamp.getTime(); @@ -282,7 +280,7 @@ class MessageEmbed { /** * Sets the title of this embed. * @param {StringResolvable} title The title - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setTitle(title) { title = Util.resolveString(title); @@ -294,7 +292,7 @@ class MessageEmbed { /** * Sets the URL of this embed. * @param {string} url The URL - * @returns {MessageEmbed} This embed + * @returns {MessageEmbed} */ setURL(url) { this.url = url; diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index b54787706..4d9c20f3e 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -118,8 +118,8 @@ class MessageMentions { } /** - * Check if a user is mentioned. Takes into account user mentions, role - * mentions, and @everyone/@here mentions. + * Check if a user 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 * @returns {boolean} diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 0284260fd..a2dd00a73 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -67,7 +67,7 @@ class Game { } /** - * Whether this game is equal to another game + * Whether this game is equal to another game. * @param {Game} game The game to compare with * @returns {boolean} */ diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index ba4eef4de..2e3f8be92 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -65,7 +65,7 @@ class ReactionCollector extends Collector { /** * Handle an incoming reaction for possible collection. * @param {MessageReaction} reaction The reaction to possibly collect - * @returns {?{key: Snowflake, value: MessageReaction}} Reaction data to collect + * @returns {?{key: Snowflake, value: MessageReaction}} * @private */ collect(reaction) { @@ -79,7 +79,7 @@ class ReactionCollector extends Collector { /** * Handle a reaction deletion for possible disposal. * @param {MessageReaction} reaction The reaction to possibly dispose - * @returns {?Snowflake|string} The reaction key + * @returns {?Snowflake|string} */ dispose(reaction) { return reaction.message.id === this.message.id && !reaction.count ? ReactionCollector.key(reaction) : null; @@ -105,7 +105,7 @@ class ReactionCollector extends Collector { /** * Get the collector key for a reaction. * @param {MessageReaction} reaction The message reaction to get the key for - * @returns {Snowflake|string} The emoji ID (if custom) or the emoji name (if native; will be unicode) + * @returns {Snowflake|string} */ static key(reaction) { return reaction.emoji.id || reaction.emoji.name; diff --git a/src/structures/Role.js b/src/structures/Role.js index 797bb7121..2db019931 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -135,7 +135,7 @@ class Role { } /** - * Get an object mapping permission names to whether or not the role enables that permission + * Get an object mapping permission names to whether or not the role enables that permission. * @returns {Object} * @example * // Print the serialized role permissions @@ -198,8 +198,8 @@ class Role { * @example * // Edit a role * role.edit({name: 'new role'}) - * .then(r => console.log(`Edited role ${r}`)) - * .catch(console.error); + * .then(r => console.log(`Edited role ${r}`)) + * .catch(console.error); */ edit(data, reason) { if (data.permissions) data.permissions = Permissions.resolve(data.permissions); @@ -226,8 +226,8 @@ class Role { * @example * // Set the name of the role * role.setName('new role') - * .then(r => console.log(`Edited name of role ${r}`)) - * .catch(console.error); + * .then(r => console.log(`Edited name of role ${r}`)) + * .catch(console.error); */ setName(name, reason) { return this.edit({ name }, reason); @@ -241,8 +241,8 @@ class Role { * @example * // Set the color of a role * role.setColor('#FF0000') - * .then(r => console.log(`Set color of role ${r}`)) - * .catch(console.error); + * .then(r => console.log(`Set color of role ${r}`)) + * .catch(console.error); */ setColor(color, reason) { return this.edit({ color }, reason); @@ -256,8 +256,8 @@ class Role { * @example * // Set the hoist of the role * role.setHoist(true) - * .then(r => console.log(`Role hoisted: ${r.hoist}`)) - * .catch(console.error); + * .then(r => console.log(`Role hoisted: ${r.hoist}`)) + * .catch(console.error); */ setHoist(hoist, reason) { return this.edit({ hoist }, reason); @@ -271,8 +271,8 @@ class Role { * @example * // Set the position of the role * role.setPosition(1) - * .then(r => console.log(`Role position: ${r.position}`)) - * .catch(console.error); + * .then(r => console.log(`Role position: ${r.position}`)) + * .catch(console.error); */ setPosition(position, relative) { return this.guild.setRolePosition(this, position, relative).then(() => this); @@ -286,8 +286,8 @@ class Role { * @example * // Set the permissions of the role * role.setPermissions(['KICK_MEMBERS', 'BAN_MEMBERS']) - * .then(r => console.log(`Role updated ${r}`)) - * .catch(console.error); + * .then(r => console.log(`Role updated ${r}`)) + * .catch(console.error); */ setPermissions(permissions, reason) { return this.edit({ permissions }, reason); @@ -301,8 +301,8 @@ class Role { * @example * // Make the role mentionable * role.setMentionable(true) - * .then(r => console.log(`Role updated ${r}`)) - * .catch(console.error); + * .then(r => console.log(`Role updated ${r}`)) + * .catch(console.error); */ setMentionable(mentionable, reason) { return this.edit({ mentionable }, reason); @@ -315,8 +315,8 @@ class Role { * @example * // Delete a role * role.delete() - * .then(r => console.log(`Deleted role ${r}`)) - * .catch(console.error); + * .then(r => console.log(`Deleted role ${r}`)) + * .catch(console.error); */ delete(reason) { return this.client.api.guilds[this.guild.id].roles[this.id].delete({ reason }) diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 7a817d089..d0dc1fd3a 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -54,9 +54,9 @@ class TextChannel extends GuildChannel { * @param {string} [reason] Reason for creating this webhook * @returns {Promise} webhook The created webhook * @example - * channel.createWebhook('Snek', 'http://snek.s3.amazonaws.com/topSnek.png') - * .then(webhook => console.log(`Created webhook ${webhook}`)) - * .catch(console.error) + * channel.createWebhook('Snek', 'https://i.imgur.com/mI8XcpG.jpg') + * .then(webhook => console.log(`Created webhook ${webhook}`)) + * .catch(console.error) */ createWebhook(name, avatar, reason) { if (typeof avatar === 'string' && avatar.startsWith('data:')) { diff --git a/src/structures/User.js b/src/structures/User.js index 0a32799c5..13ed16a21 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -105,7 +105,7 @@ class User { } /** - * A link to the user's avatar + * A link to the user's avatar. * @param {Object} [options={}] Options for the avatar url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, * it will be `gif` for animated avatars or otherwise `webp` @@ -127,7 +127,8 @@ class User { } /** - * A link to the user's avatar if they have one. Otherwise a link to their default avatar will be returned + * A link to the user's avatar if they have one. + * Otherwise a link to their default avatar will be returned. * @param {Object} [options={}] Options for the avatar url * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, * it will be `gif` for animated avatars or otherwise `webp` diff --git a/src/structures/UserProfile.js b/src/structures/UserProfile.js index f2c9cea03..76ebb8f47 100644 --- a/src/structures/UserProfile.js +++ b/src/structures/UserProfile.js @@ -14,7 +14,7 @@ class UserProfile { this.user = user; /** - * The client that created the instance of the UserProfile. + * The client that created the instance of the UserProfile * @name UserProfile#client * @type {Client} * @readonly diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 2a67c9379..5234b600e 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -82,8 +82,8 @@ class VoiceChannel extends GuildChannel { * @example * // Set the bitrate of a voice channel * voiceChannel.setBitrate(48) - * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`)) - * .catch(console.error); + * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`)) + * .catch(console.error); */ setBitrate(bitrate, reason) { bitrate *= 1000; @@ -98,8 +98,8 @@ class VoiceChannel extends GuildChannel { * @example * // Set the user limit of a voice channel * voiceChannel.setUserLimit(42) - * .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`)) - * .catch(console.error); + * .then(vc => console.log(`Set user limit to ${vc.userLimit} for ${vc.name}`)) + * .catch(console.error); */ setUserLimit(userLimit, reason) { return this.edit({ userLimit }, reason); @@ -111,8 +111,8 @@ class VoiceChannel extends GuildChannel { * @example * // Join a voice channel * voiceChannel.join() - * .then(connection => console.log('Connected!')) - * .catch(console.error); + * .then(connection => console.log('Connected!')) + * .catch(console.error); */ join() { if (this.client.browser) return Promise.reject(new Error('VOICE_NO_BROWSER')); diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index a42c3396b..281e98868 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -99,8 +99,8 @@ class Webhook { * @example * // Send a message * webhook.send('hello!') - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); */ send(content, options) { // eslint-disable-line complexity if (!options && typeof content === 'object' && !(content instanceof Array)) { diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index a50609d2c..df17aac6c 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -71,8 +71,8 @@ class TextBasedChannel { * @example * // Send a message * channel.send('hello!') - * .then(message => console.log(`Sent message: ${message.content}`)) - * .catch(console.error); + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); */ send(content, options) { // eslint-disable-line complexity if (!options && typeof content === 'object' && !(content instanceof Array)) { @@ -182,8 +182,8 @@ class TextBasedChannel { * @example * // Get messages * channel.fetchMessages({limit: 10}) - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); */ fetchMessages(options = {}) { const Message = require('../Message'); @@ -308,8 +308,8 @@ class TextBasedChannel { * @example * // Create a message collector * const collector = channel.createMessageCollector( - * m => m.content.includes('discord'), - * { time: 15000 } + * m => m.content.includes('discord'), + * { time: 15000 } * ); * collector.on('collect', m => console.log(`Collected ${m.content}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); @@ -335,8 +335,8 @@ class TextBasedChannel { * const filter = m => m.content.startsWith('!vote'); * // Errors: ['time'] treats ending because of the time limit as an error * channel.awaitMessages(filter, { max: 4, time: 60000, errors: ['time'] }) - * .then(collected => console.log(collected.size)) - * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); + * .then(collected => console.log(collected.size)) + * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); */ awaitMessages(filter, options = {}) { return new Promise((resolve, reject) => { diff --git a/src/util/Constants.js b/src/util/Constants.js index 5651614c7..3e3ad6755 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -129,12 +129,12 @@ exports.Endpoints = { /** * The current status of the client. Here are the available statuses: - * - READY - * - CONNECTING - * - RECONNECTING - * - IDLE - * - NEARLY - * - DISCONNECTED + * * READY + * * CONNECTING + * * RECONNECTING + * * IDLE + * * NEARLY + * * DISCONNECTED * @typedef {number} Status */ exports.Status = { @@ -148,11 +148,11 @@ exports.Status = { /** * The current status of a voice connection. Here are the available statuses: - * - CONNECTED - * - CONNECTING - * - AUTHENTICATING - * - RECONNECTING - * - DISCONNECTED + * * CONNECTED + * * CONNECTING + * * AUTHENTICATING + * * RECONNECTING + * * DISCONNECTED * @typedef {number} VoiceStatus */ exports.VoiceStatus = { @@ -243,41 +243,41 @@ exports.Events = { /** * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: - * - READY - * - RESUMED - * - GUILD_SYNC - * - GUILD_CREATE - * - GUILD_DELETE - * - GUILD_UPDATE - * - GUILD_MEMBER_ADD - * - GUILD_MEMBER_REMOVE - * - GUILD_MEMBER_UPDATE - * - GUILD_MEMBERS_CHUNK - * - GUILD_ROLE_CREATE - * - GUILD_ROLE_DELETE - * - GUILD_ROLE_UPDATE - * - GUILD_BAN_ADD - * - GUILD_BAN_REMOVE - * - CHANNEL_CREATE - * - CHANNEL_DELETE - * - CHANNEL_UPDATE - * - CHANNEL_PINS_UPDATE - * - MESSAGE_CREATE - * - MESSAGE_DELETE - * - MESSAGE_UPDATE - * - MESSAGE_DELETE_BULK - * - MESSAGE_REACTION_ADD - * - MESSAGE_REACTION_REMOVE - * - MESSAGE_REACTION_REMOVE_ALL - * - USER_UPDATE - * - USER_NOTE_UPDATE - * - USER_SETTINGS_UPDATE - * - PRESENCE_UPDATE - * - VOICE_STATE_UPDATE - * - TYPING_START - * - VOICE_SERVER_UPDATE - * - RELATIONSHIP_ADD - * - RELATIONSHIP_REMOVE + * * READY + * * RESUMED + * * GUILD_SYNC + * * GUILD_CREATE + * * GUILD_DELETE + * * GUILD_UPDATE + * * GUILD_MEMBER_ADD + * * GUILD_MEMBER_REMOVE + * * GUILD_MEMBER_UPDATE + * * GUILD_MEMBERS_CHUNK + * * GUILD_ROLE_CREATE + * * GUILD_ROLE_DELETE + * * GUILD_ROLE_UPDATE + * * GUILD_BAN_ADD + * * GUILD_BAN_REMOVE + * * CHANNEL_CREATE + * * CHANNEL_DELETE + * * CHANNEL_UPDATE + * * CHANNEL_PINS_UPDATE + * * MESSAGE_CREATE + * * MESSAGE_DELETE + * * MESSAGE_UPDATE + * * MESSAGE_DELETE_BULK + * * MESSAGE_REACTION_ADD + * * MESSAGE_REACTION_REMOVE + * * MESSAGE_REACTION_REMOVE_ALL + * * USER_UPDATE + * * USER_NOTE_UPDATE + * * USER_SETTINGS_UPDATE + * * PRESENCE_UPDATE + * * VOICE_STATE_UPDATE + * * TYPING_START + * * VOICE_SERVER_UPDATE + * * RELATIONSHIP_ADD + * * RELATIONSHIP_REMOVE * @typedef {string} WSEventType */ exports.WSEvents = { @@ -322,14 +322,14 @@ exports.WSEvents = { /** * The type of a message, e.g. `DEFAULT`. Here are the available types: - * - DEFAULT - * - RECIPIENT_ADD - * - RECIPIENT_REMOVE - * - CALL - * - CHANNEL_NAME_CHANGE - * - CHANNEL_ICON_CHANGE - * - PINS_ADD - * - GUILD_MEMBER_JOIN + * * DEFAULT + * * RECIPIENT_ADD + * * RECIPIENT_REMOVE + * * CALL + * * CHANNEL_NAME_CHANGE + * * CHANNEL_ICON_CHANGE + * * PINS_ADD + * * GUILD_MEMBER_JOIN * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -345,10 +345,10 @@ exports.MessageTypes = [ /** * The type of a game of a users presence, e.g. `PLAYING`. Here are the available types: - * - PLAYING - * - STREAMING - * - LISTENING - * - WATCHING + * * PLAYING + * * STREAMING + * * LISTENING + * * WATCHING * @typedef {string} GameType */ exports.GameTypes = [ @@ -560,9 +560,9 @@ exports.UserChannelOverrideMap = { /** * All flags users can have: - * - STAFF - * - PARTNER - * - HYPESQUAD + * * STAFF + * * PARTNER + * * HYPESQUAD * @typedef {string} UserFlags */ exports.UserFlags = { @@ -606,49 +606,49 @@ exports.Colors = { /** * An error encountered while performing an API request. Here are the potential errors: - * - UNKNOWN_ACCOUNT - * - UNKNOWN_APPLICATION - * - UNKNOWN_CHANNEL - * - UNKNOWN_GUILD - * - UNKNOWN_INTEGRATION - * - UNKNOWN_INVITE - * - UNKNOWN_MEMBER - * - UNKNOWN_MESSAGE - * - UNKNOWN_OVERWRITE - * - UNKNOWN_PROVIDER - * - UNKNOWN_ROLE - * - UNKNOWN_TOKEN - * - UNKNOWN_USER - * - UNKNOWN_EMOJI - * - BOT_PROHIBITED_ENDPOINT - * - BOT_ONLY_ENDPOINT - * - MAXIMUM_GUILDS - * - MAXIMUM_FRIENDS - * - MAXIMUM_PINS - * - MAXIMUM_ROLES - * - MAXIMUM_REACTIONS - * - UNAUTHORIZED - * - MISSING_ACCESS - * - INVALID_ACCOUNT_TYPE - * - CANNOT_EXECUTE_ON_DM - * - EMBED_DISABLED - * - CANNOT_EDIT_MESSAGE_BY_OTHER - * - CANNOT_SEND_EMPTY_MESSAGE - * - CANNOT_MESSAGE_USER - * - CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL - * - CHANNEL_VERIFICATION_LEVEL_TOO_HIGH - * - OAUTH2_APPLICATION_BOT_ABSENT - * - MAXIMUM_OAUTH2_APPLICATIONS - * - INVALID_OAUTH_STATE - * - MISSING_PERMISSIONS - * - INVALID_AUTHENTICATION_TOKEN - * - NOTE_TOO_LONG - * - INVALID_BULK_DELETE_QUANTITY - * - CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL - * - CANNOT_EXECUTE_ON_SYSTEM_MESSAGE - * - BULK_DELETE_MESSAGE_TOO_OLD - * - INVITE_ACCEPTED_TO_GUILD_NOT_CONTANING_BOT - * - REACTION_BLOCKED + * * UNKNOWN_ACCOUNT + * * UNKNOWN_APPLICATION + * * UNKNOWN_CHANNEL + * * UNKNOWN_GUILD + * * UNKNOWN_INTEGRATION + * * UNKNOWN_INVITE + * * UNKNOWN_MEMBER + * * UNKNOWN_MESSAGE + * * UNKNOWN_OVERWRITE + * * UNKNOWN_PROVIDER + * * UNKNOWN_ROLE + * * UNKNOWN_TOKEN + * * UNKNOWN_USER + * * UNKNOWN_EMOJI + * * BOT_PROHIBITED_ENDPOINT + * * BOT_ONLY_ENDPOINT + * * MAXIMUM_GUILDS + * * MAXIMUM_FRIENDS + * * MAXIMUM_PINS + * * MAXIMUM_ROLES + * * MAXIMUM_REACTIONS + * * UNAUTHORIZED + * * MISSING_ACCESS + * * INVALID_ACCOUNT_TYPE + * * CANNOT_EXECUTE_ON_DM + * * EMBED_DISABLED + * * CANNOT_EDIT_MESSAGE_BY_OTHER + * * CANNOT_SEND_EMPTY_MESSAGE + * * CANNOT_MESSAGE_USER + * * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL + * * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH + * * OAUTH2_APPLICATION_BOT_ABSENT + * * MAXIMUM_OAUTH2_APPLICATIONS + * * INVALID_OAUTH_STATE + * * MISSING_PERMISSIONS + * * INVALID_AUTHENTICATION_TOKEN + * * NOTE_TOO_LONG + * * INVALID_BULK_DELETE_QUANTITY + * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL + * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE + * * BULK_DELETE_MESSAGE_TOO_OLD + * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTANING_BOT + * * REACTION_BLOCKED * @typedef {string} APIError */ exports.APIErrors = { diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 679e3c6e1..d4678ff1c 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -69,7 +69,7 @@ class Permissions { } /** - * Gets an object mapping permission name (like `READ_MESSAGES`) to a {@link boolean} indicating whether the + * Gets an object mapping permission name (like `VIEW_CHANNEL`) to a {@link boolean} indicating whether the * permission is available. * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override * @returns {Object} @@ -82,8 +82,8 @@ class Permissions { /** * Data that can be resolved to give a permission number. This can be: - * - A string (see {@link Permissions.FLAGS}) - * - A permission number + * * A string (see {@link Permissions.FLAGS}) + * * A permission number * @typedef {string|number} PermissionResolvable */ @@ -102,34 +102,34 @@ class Permissions { /** * Numeric permission flags. All available properties: - * - `ADMINISTRATOR` (implicitly has *all* permissions, and bypasses all channel overwrites) - * - `CREATE_INSTANT_INVITE` (create invitations to the guild) - * - `KICK_MEMBERS` - * - `BAN_MEMBERS` - * - `MANAGE_CHANNELS` (edit and reorder channels) - * - `MANAGE_GUILD` (edit the guild information, region, etc.) - * - `ADD_REACTIONS` (add new reactions to messages) - * - `VIEW_AUDIT_LOG` - * - `VIEW_CHANNELS` - * - `SEND_MESSAGES` - * - `SEND_TTS_MESSAGES` - * - `MANAGE_MESSAGES` (delete messages and reactions) - * - `EMBED_LINKS` (links posted will have a preview embedded) - * - `ATTACH_FILES` - * - `READ_MESSAGE_HISTORY` (view messages that were posted prior to opening Discord) - * - `MENTION_EVERYONE` - * - `USE_EXTERNAL_EMOJIS` (use emojis from different guilds) - * - `CONNECT` (connect to a voice channel) - * - `SPEAK` (speak in a voice channel) - * - `MUTE_MEMBERS` (mute members across all voice channels) - * - `DEAFEN_MEMBERS` (deafen members across all voice channels) - * - `MOVE_MEMBERS` (move members between voice channels) - * - `USE_VAD` (use voice activity detection) - * - `CHANGE_NICKNAME` - * - `MANAGE_NICKNAMES` (change other members' nicknames) - * - `MANAGE_ROLES` - * - `MANAGE_WEBHOOKS` - * - `MANAGE_EMOJIS` + * * `ADMINISTRATOR` (implicitly has *all* permissions, and bypasses all channel overwrites) + * * `CREATE_INSTANT_INVITE` (create invitations to the guild) + * * `KICK_MEMBERS` + * * `BAN_MEMBERS` + * * `MANAGE_CHANNELS` (edit and reorder channels) + * * `MANAGE_GUILD` (edit the guild information, region, etc.) + * * `ADD_REACTIONS` (add new reactions to messages) + * * `VIEW_AUDIT_LOG` + * * `VIEW_CHANNEL` + * * `SEND_MESSAGES` + * * `SEND_TTS_MESSAGES` + * * `MANAGE_MESSAGES` (delete messages and reactions) + * * `EMBED_LINKS` (links posted will have a preview embedded) + * * `ATTACH_FILES` + * * `READ_MESSAGE_HISTORY` (view messages that were posted prior to opening Discord) + * * `MENTION_EVERYONE` + * * `USE_EXTERNAL_EMOJIS` (use emojis from different guilds) + * * `CONNECT` (connect to a voice channel) + * * `SPEAK` (speak in a voice channel) + * * `MUTE_MEMBERS` (mute members across all voice channels) + * * `DEAFEN_MEMBERS` (deafen members across all voice channels) + * * `MOVE_MEMBERS` (move members between voice channels) + * * `USE_VAD` (use voice activity detection) + * * `CHANGE_NICKNAME` + * * `MANAGE_NICKNAMES` (change other members' nicknames) + * * `MANAGE_ROLES` + * * `MANAGE_WEBHOOKS` + * * `MANAGE_EMOJIS` * @type {Object} * @see {@link https://discordapp.com/developers/docs/topics/permissions} */ diff --git a/src/util/Util.js b/src/util/Util.js index 2023050d0..1a710eea8 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -70,9 +70,9 @@ class Util { /** * Parses emoji info out of a string. The string must be one of: - * - A UTF-8 emoji (no ID) - * - A URL-encoded UTF-8 emoji (no ID) - * - A Discord custom emoji (`<:name:id>`) + * * A UTF-8 emoji (no ID) + * * A URL-encoded UTF-8 emoji (no ID) + * * A Discord custom emoji (`<:name:id>`) * @param {string} text Emoji string to parse * @returns {Object} Object with `name` and `id` properties * @private From fff8b764aff39751dad5f3b25b0e3acc84717b4a Mon Sep 17 00:00:00 2001 From: iCrawl Date: Thu, 17 Aug 2017 20:20:20 +0200 Subject: [PATCH 0210/1359] Update package.json --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 991055799..b0d6b0230 100644 --- a/package.json +++ b/package.json @@ -32,19 +32,19 @@ "homepage": "https://github.com/hydrabolt/discord.js#readme", "runkitExampleFilename": "./docs/examples/ping.js", "dependencies": { - "long": "^3.2.0", + "long": "^3.0.0", "prism-media": "^0.0.1", - "snekfetch": "^3.2.0", + "snekfetch": "^3.0.0", "tweetnacl": "^1.0.0", "ws": "^3.0.0" }, "peerDependencies": { "bufferutil": "^3.0.0", "erlpack": "hammerandchisel/erlpack", - "node-opus": "^0.2.5", + "node-opus": "^0.2.0", "opusscript": "^0.0.3", - "sodium": "^2.0.1", - "libsodium-wrappers": "^0.5.1", + "sodium": "^2.0.0", + "libsodium-wrappers": "^0.5.0", "uws": "^8.14.0" }, "devDependencies": { From 9b97fe292f62f18303c8a872511ce61f683e9dc3 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Thu, 17 Aug 2017 11:49:41 -0700 Subject: [PATCH 0211/1359] Remove partial classes (#1794) * remove partial objects * remove partial evil * Update Invite.js * Update Invite.js --- src/index.js | 2 -- src/structures/Guild.js | 4 +-- src/structures/Invite.js | 19 +++++----- src/structures/PartialGuild.js | 51 --------------------------- src/structures/PartialGuildChannel.js | 44 ----------------------- src/structures/VoiceChannel.js | 2 +- 6 files changed, 12 insertions(+), 110 deletions(-) delete mode 100644 src/structures/PartialGuild.js delete mode 100644 src/structures/PartialGuildChannel.js diff --git a/src/index.js b/src/index.js index 217bb1494..d4f05a60f 100644 --- a/src/index.js +++ b/src/index.js @@ -47,8 +47,6 @@ module.exports = { MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), ClientApplication: require('./structures/ClientApplication'), - PartialGuild: require('./structures/PartialGuild'), - PartialGuildChannel: require('./structures/PartialGuildChannel'), PermissionOverwrites: require('./structures/PermissionOverwrites'), Presence: require('./structures/Presence').Presence, ReactionEmoji: require('./structures/ReactionEmoji'), diff --git a/src/structures/Guild.js b/src/structures/Guild.js index f3e2e6257..e562cd46b 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -80,7 +80,7 @@ class Guild { * @param {*} data The raw data of the guild * @private */ - setup(data) { + setup(data) { // eslint-disable-line complexity /** * The name of the guild * @type {string} @@ -225,7 +225,7 @@ class Guild { * @type {Collection} */ this.emojis = new Collection(); - for (const emoji of data.emojis) this.emojis.set(emoji.id, new Emoji(this, emoji)); + if (data.emojis) for (const emoji of data.emojis) this.emojis.set(emoji.id, new Emoji(this, emoji)); } else { this.client.actions.GuildEmojisUpdate.handle({ guild_id: this.id, diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 53a94f081..fec7d8bf0 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -1,5 +1,3 @@ -const PartialGuild = require('./PartialGuild'); -const PartialGuildChannel = require('./PartialGuildChannel'); const Constants = require('../util/Constants'); /** @@ -20,12 +18,14 @@ class Invite { } setup(data) { + const Guild = require('./Guild'); + const Channel = require('./Channel'); + /** - * The guild the invite is for. If this guild is already known, this will be a guild object. If the guild is - * unknown, this will be a PartialGuild object - * @type {Guild|PartialGuild} + * The guild the invite is for + * @type {Guild} */ - this.guild = this.client.guilds.get(data.guild.id) || new PartialGuild(this.client, data.guild); + this.guild = this.client.guilds.get(data.guild.id) || new Guild(this.client, data.guild); /** * The code for this invite @@ -90,11 +90,10 @@ class Invite { } /** - * The channel the invite is for. If this channel is already known, this will be a GuildChannel object. - * If the channel is unknown, this will be a PartialGuildChannel object. - * @type {GuildChannel|PartialGuildChannel} + * The channel the invite is for + * @type {GuildChannel} */ - this.channel = this.client.channels.get(data.channel.id) || new PartialGuildChannel(this.client, data.channel); + this.channel = this.client.channels.get(data.channel.id) || Channel.create(this.client, data.channel, this.guild); /** * The timestamp the invite was created at diff --git a/src/structures/PartialGuild.js b/src/structures/PartialGuild.js deleted file mode 100644 index 3eb64f26a..000000000 --- a/src/structures/PartialGuild.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -{ splash: null, - id: '123123123', - icon: '123123123', - name: 'name' } -*/ - -/** - * Represents a guild that the client only has limited information for - e.g. from invites. - */ -class PartialGuild { - constructor(client, data) { - /** - * The client that instantiated this PartialGuild - * @name PartialGuild#client - * @type {Client} - * @readonly - */ - Object.defineProperty(this, 'client', { value: client }); - - this.setup(data); - } - - setup(data) { - /** - * The ID of this guild - * @type {Snowflake} - */ - this.id = data.id; - - /** - * The name of this guild - * @type {string} - */ - this.name = data.name; - - /** - * The hash of this guild's icon - * @type {?string} - */ - this.icon = data.icon; - - /** - * The hash of the guild splash image (VIP only) - * @type {?string} - */ - this.splash = data.splash; - } -} - -module.exports = PartialGuild; diff --git a/src/structures/PartialGuildChannel.js b/src/structures/PartialGuildChannel.js deleted file mode 100644 index c30c05466..000000000 --- a/src/structures/PartialGuildChannel.js +++ /dev/null @@ -1,44 +0,0 @@ -const Constants = require('../util/Constants'); - -/* -{ type: 0, id: '123123', name: 'heavy-testing' } } -*/ - -/** - * Represents a guild channel that the client only has limited information for - e.g. from invites. - */ -class PartialGuildChannel { - constructor(client, data) { - /** - * The client that instantiated this PartialGuildChannel - * @name PartialGuildChannel#client - * @type {Client} - * @readonly - */ - Object.defineProperty(this, 'client', { value: client }); - - this.setup(data); - } - - setup(data) { - /** - * The ID of this guild channel - * @type {Snowflake} - */ - this.id = data.id; - - /** - * The name of this guild channel - * @type {string} - */ - this.name = data.name; - - /** - * The type of this guild channel - `text` or `voice` - * @type {string} - */ - this.type = Constants.ChannelTypes.TEXT === data.type ? 'text' : 'voice'; - } -} - -module.exports = PartialGuildChannel; diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 5234b600e..9b7bd7e79 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -14,7 +14,7 @@ class VoiceChannel extends GuildChannel { * The members in this voice channel * @type {Collection} */ - this.members = new Collection(); + Object.defineProperty(this, 'members', { value: new Collection() }); } setup(data) { From 55543754b9aaed568a61f67a5a50f93ab07307f6 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 19 Aug 2017 20:14:49 -0700 Subject: [PATCH 0212/1359] proper fix for #1685 (#1805) * Update WebSocketConnection.js * Update WebSocketConnection.js * Update WebSocketConnection.js * Update RESTManager.js --- src/client/rest/RESTManager.js | 4 ++-- src/client/websocket/WebSocketConnection.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/rest/RESTManager.js b/src/client/rest/RESTManager.js index 530623245..7772fa815 100644 --- a/src/client/rest/RESTManager.js +++ b/src/client/rest/RESTManager.js @@ -18,8 +18,8 @@ class RESTManager { } destroy() { - for (const handlerID in this.handlers) { - this.handlers[handlerID].destroy(); + for (const handler of Object.values(this.handlers)) { + if (handler.destroy) handler.destroy(); } } diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index 6f977f395..493e4b3c4 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -81,8 +81,8 @@ class WebSocketConnection extends EventEmitter { */ this.ratelimit = { queue: [], - remaining: 120, - resetTime: -1, + remaining: 60, + resetTimer: null, }; this.connect(gateway); @@ -191,10 +191,10 @@ class WebSocketConnection extends EventEmitter { if (this.ratelimit.remaining === 0) return; if (this.ratelimit.queue.length === 0) return; if (this.ratelimit.remaining === 120) { - this.ratelimit.resetTimer = setTimeout(() => { - this.ratelimit.remaining = 120; + this.ratelimit.resetTimer = this.client.setTimeout(() => { + this.ratelimit.remaining = 60; this.processQueue(); - }, 120e3); // eslint-disable-line + }, 120e3); } while (this.ratelimit.remaining > 0) { const item = this.ratelimit.queue.shift(); From 8c855855cd5170dfcb48c9a0fcd5a81e7e51eb8f Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 19 Aug 2017 20:15:02 -0700 Subject: [PATCH 0213/1359] Guild/systemchannel (#1799) * add cool system channel * Update Guild.js * Update Guild.js * Update Guild.js --- src/structures/Guild.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index e562cd46b..3b60e68be 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -141,6 +141,12 @@ class Guild { */ this.afkChannelID = data.afk_channel_id; + /** + * The ID of the system channel + * @type {?Snowflake} + */ + this.systemChannelID = data.system_channel_id; + /** * Whether embedded images are enabled on this guild * @type {boolean} @@ -303,6 +309,24 @@ class Guild { return this.members.get(this.ownerID); } + /** + * AFK voice channel for this guild + * @type {?VoiceChannel} + * @readonly + */ + get afkChannel() { + return this.client.channels.get(this.afkChannelID); + } + + /** + * System channel for this guild + * @type {?GuildChannel} + * @readonly + */ + get systemChannel() { + return this.client.channels.get(this.systemChannelID); + } + /** * If the client is connected to any voice channel in this guild, this will be the relevant VoiceConnection * @type {?VoiceConnection} @@ -650,6 +674,7 @@ class Guild { if (data.region) _data.region = data.region; if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel); if (data.afkChannel) _data.afk_channel_id = this.client.resolver.resolveChannel(data.afkChannel).id; + if (data.systemChannel) _data.system_channel_id = this.client.resolver.resolveChannel(data.systemChannel).id; if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); if (data.icon) _data.icon = this.client.resolver.resolveBase64(data.icon); if (data.owner) _data.owner_id = this.client.resolver.resolveUser(data.owner).id; @@ -731,6 +756,16 @@ class Guild { return this.edit({ afkChannel }, reason); } + /** + * Edit the system channel of the guild. + * @param {ChannelResolvable} systemChannel The new system channel + * @param {string} [reason] Reason for changing the guild's system channel + * @returns {Promise} + */ + setSystemChannel(systemChannel, reason) { + return this.edit({ systemChannel }, reason); + } + /** * Edit the AFK timeout of the guild. * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK From 8b8a365e99f8c86d1dc4a49c863b825b10203435 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 20 Aug 2017 01:14:38 -0700 Subject: [PATCH 0214/1359] fix ratelimits (#1806) --- src/client/websocket/WebSocketConnection.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index 493e4b3c4..436988a4d 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -82,6 +82,7 @@ class WebSocketConnection extends EventEmitter { this.ratelimit = { queue: [], remaining: 60, + total: 60, resetTimer: null, }; this.connect(gateway); @@ -190,9 +191,9 @@ class WebSocketConnection extends EventEmitter { processQueue() { if (this.ratelimit.remaining === 0) return; if (this.ratelimit.queue.length === 0) return; - if (this.ratelimit.remaining === 120) { + if (this.ratelimit.remaining === this.ratelimit.total) { this.ratelimit.resetTimer = this.client.setTimeout(() => { - this.ratelimit.remaining = 60; + this.ratelimit.remaining = this.ratelimit.total; this.processQueue(); }, 120e3); } @@ -276,6 +277,7 @@ class WebSocketConnection extends EventEmitter { this.packetManager.handleQueue(); this.ws = null; this.status = Constants.Status.DISCONNECTED; + this.ratelimit.remaining = this.ratelimit.total; return true; } From 9c2e3b8978ac0e82309c11eb838cf702473bc0df Mon Sep 17 00:00:00 2001 From: bdistin Date: Sun, 20 Aug 2017 16:13:08 -0500 Subject: [PATCH 0215/1359] Pass the collection of X collected in collectors (#1594) ...to the filter function. --- src/structures/interfaces/Collector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 017c3797d..73bbcde59 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -5,6 +5,7 @@ const EventEmitter = require('events'); * Filter to be applied to the collector. * @typedef {Function} CollectorFilter * @param {...*} args Any arguments received by the listener + * @param {Collection} collection The items collected by this collector * @returns {boolean} To collect or not collect */ @@ -75,7 +76,7 @@ class Collector extends EventEmitter { */ handleCollect(...args) { const collect = this.collect(...args); - if (!collect || !this.filter(...args)) return; + if (!collect || !this.filter(...args, this.collected)) return; this.collected.set(collect.key, collect.value); From 2c763073d700ea3f00535b011b3090f393765b81 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Sun, 20 Aug 2017 14:26:13 -0700 Subject: [PATCH 0216/1359] add capture groups to MessageMentions and validate snowflake ranges (#1612) * add capture groups * update patterns to valid snowflake ranges --- src/structures/MessageMentions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 4d9c20f3e..f6144ae30 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -144,18 +144,18 @@ MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; * Regular expression that globally matches user mentions like `<@81440962496172032>` * @type {RegExp} */ -MessageMentions.USERS_PATTERN = /<@!?[0-9]+>/g; +MessageMentions.USERS_PATTERN = /<@!?(1|\d{17,19})>/g; /** * Regular expression that globally matches role mentions like `<@&297577916114403338>` * @type {RegExp} */ -MessageMentions.ROLES_PATTERN = /<@&[0-9]+>/g; +MessageMentions.ROLES_PATTERN = /<@&(\d{17,19})>/g; /** * Regular expression that globally matches channel mentions like `<#222079895583457280>` * @type {RegExp} */ -MessageMentions.CHANNELS_PATTERN = /<#([0-9]+)>/g; +MessageMentions.CHANNELS_PATTERN = /<#(\d{17,19})>/g; module.exports = MessageMentions; From 0c0ec72cb88ceb679190e4bbdca2040fd2e2d4d0 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sun, 20 Aug 2017 16:36:22 -0500 Subject: [PATCH 0217/1359] Fix Guild#createRole with position (#1720) * Fix Guild#createRole with position + validation * remove position validation, small update * fixed async issue if position provided --- src/structures/Guild.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 3b60e68be..1ed7864ad 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1043,6 +1043,7 @@ class Guild { /** * Creates a new role in the guild with given information + * The position will silently reset to 1 if an invalid one is provided, or none. * @param {Object} [options] Options * @param {RoleData} [options.data] The data to update the role with * @param {string} [options.reason] Reason for creating this role @@ -1068,12 +1069,14 @@ class Guild { if (data.color) data.color = Util.resolveColor(data.color); if (data.permissions) data.permissions = Permissions.resolve(data.permissions); - return this.client.api.guilds(this.id).roles.post({ data, reason }).then(role => - this.client.actions.GuildRoleCreate.handle({ + return this.client.api.guilds(this.id).roles.post({ data, reason }).then(r => { + const { role } = this.client.actions.GuildRoleCreate.handle({ guild_id: this.id, - role, - }).role - ); + role: r, + }); + if (data.position) return role.setPosition(data.position, reason); + return role; + }); } /** From 4520c801d370708bc1d914bfa2d9b61729dbdf1e Mon Sep 17 00:00:00 2001 From: Isabella Date: Mon, 21 Aug 2017 16:11:58 -0500 Subject: [PATCH 0218/1359] update attachment & docs (#1815) --- src/client/ClientDataResolver.js | 38 +++++++------------ src/structures/Attachment.js | 13 ++++--- src/structures/Webhook.js | 2 +- src/structures/interfaces/TextBasedChannel.js | 2 +- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js index 1cc447708..687e0afe0 100644 --- a/src/client/ClientDataResolver.js +++ b/src/client/ClientDataResolver.js @@ -202,12 +202,17 @@ class ClientDataResolver { * @typedef {string|Buffer} BufferResolvable */ + /** + * @external Stream + * @see {@link https://nodejs.org/api/stream.html} + */ + /** * Resolves a BufferResolvable to a Buffer. - * @param {BufferResolvable} resource The buffer resolvable to resolve + * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve * @returns {Promise} */ - resolveBuffer(resource) { + resolveFile(resource) { if (resource instanceof Buffer) return Promise.resolve(resource); if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(Util.convertToBuffer(resource)); @@ -232,33 +237,18 @@ class ClientDataResolver { }); } }); + } else if (resource.pipe && typeof resource.pipe === 'function') { + return new Promise((resolve, reject) => { + const buffers = []; + resource.once('error', reject); + resource.on('data', data => buffers.push(data)); + resource.once('end', () => resolve(Buffer.concat(buffers))); + }); } return Promise.reject(new TypeError('REQ_RESOURCE_TYPE')); } - /** - * Converts a Stream to a Buffer. - * @param {Stream} resource The stream to convert - * @returns {Promise} - */ - resolveFile(resource) { - return resource ? this.resolveBuffer(resource) - .catch(() => { - if (resource.pipe && typeof resource.pipe === 'function') { - return new Promise((resolve, reject) => { - const buffers = []; - resource.once('error', reject); - resource.on('data', data => buffers.push(data)); - resource.once('end', () => resolve(Buffer.concat(buffers))); - }); - } else { - throw new TypeError('REQ_RESOURCE_TYPE'); - } - }) : - Promise.reject(new TypeError('REQ_RESOURCE_TYPE')); - } - /** * Data that can be resolved to give an emoji identifier. This can be: * * The unicode representation of an emoji diff --git a/src/structures/Attachment.js b/src/structures/Attachment.js index fd4fa8f80..e4ccbb197 100644 --- a/src/structures/Attachment.js +++ b/src/structures/Attachment.js @@ -1,10 +1,13 @@ /** * Represents an attachment in a message. + * @param {BufferResolvable|Stream} file The file + * @param {string} [name] The name of the file, if any */ class Attachment { constructor(file, name) { this.file = null; - this._attach(file, name); + if (name) this.setAttachment(file, name); + else this._attach(file); } /** @@ -42,7 +45,7 @@ class Attachment { * @returns {Attachment} This attachment */ setFile(attachment) { - this.file.attachment = attachment; + this.file = { attachment }; return this; } @@ -63,10 +66,8 @@ class Attachment { * @private */ _attach(file, name) { - if (file) { - if (typeof file === 'string') this.file = file; - else this.setAttachment(file, name); - } + if (typeof file === 'string') this.file = file; + else this.setAttachment(file, name); } } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 281e98868..a179f545b 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -94,7 +94,7 @@ class Webhook { /** * Send a message with this webhook. * @param {StringResolvable} [content] The content to send - * @param {WebhookMessageOptions} [options={}] The options to provide + * @param {WebhookMessageOptions|MessageEmbed|Attachment|Attachment[]} [options={}] The options to provide * @returns {Promise} * @example * // Send a message diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index df17aac6c..0bae9cbe9 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -66,7 +66,7 @@ class TextBasedChannel { /** * Send a message to this channel. * @param {StringResolvable} [content] Text for the message - * @param {MessageOptions} [options={}] Options for the message + * @param {MessageOptions|MessageEmbed|Attachment|Attachment[]} [options={}] Options for the message * @returns {Promise} * @example * // Send a message From b055dae9989a216e64f0d7f232d00d409ab002ed Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 22 Aug 2017 10:55:28 -0700 Subject: [PATCH 0219/1359] make webpack over 9000 times better (#1816) * webpack stuff * even better * Update browser.js --- browser.js | 9 +++++++++ src/client/Client.js | 3 +-- src/client/websocket/WebSocketConnection.js | 2 +- src/index.js | 2 -- webpack.config.js | 7 ++++++- 5 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 browser.js diff --git a/browser.js b/browser.js new file mode 100644 index 000000000..9f9341efc --- /dev/null +++ b/browser.js @@ -0,0 +1,9 @@ +const browser = typeof window !== 'undefined'; +const webpack = !!process.env.__DISCORD_WEBPACK__; + +const Discord = require('./'); + +module.exports = Discord; +if (browser && webpack) window.Discord = Discord; // eslint-disable-line no-undef +// eslint-disable-next-line no-console +else if (!browser) console.warn('Warning: Attempting to use browser version of Discord.js in a non-browser environment!'); diff --git a/src/client/Client.js b/src/client/Client.js index ae40d6774..7e1df3dc5 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,4 +1,3 @@ -const os = require('os'); const EventEmitter = require('events'); const Constants = require('../util/Constants'); const Permissions = require('../util/Permissions'); @@ -265,7 +264,7 @@ class Client extends EventEmitter { * @readonly */ get browser() { - return os.platform() === 'browser'; + return typeof window !== 'undefined'; } /** diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index 436988a4d..5620e480e 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -1,4 +1,4 @@ -const browser = require('os').platform() === 'browser'; +const browser = typeof window !== 'undefined'; const EventEmitter = require('events'); const Constants = require('../../util/Constants'); const zlib = require('zlib'); diff --git a/src/index.js b/src/index.js index d4f05a60f..9d1f78fa0 100644 --- a/src/index.js +++ b/src/index.js @@ -57,5 +57,3 @@ module.exports = { VoiceChannel: require('./structures/VoiceChannel'), Webhook: require('./structures/Webhook'), }; - -if (require('os').platform() === 'browser') window.Discord = module.exports; // eslint-disable-line no-undef diff --git a/webpack.config.js b/webpack.config.js index 55d0e12f6..db75a5e23 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,11 @@ const createConfig = options => { const plugins = [ new webpack.DefinePlugin({ 'global.GENTLY': false }), new webpack.optimize.ModuleConcatenationPlugin(), + new webpack.DefinePlugin({ + 'process.env': { + __DISCORD_WEBPACK__: '"true"', + }, + }), ]; if (options.minify) plugins.push(new UglifyJSPlugin({ uglifyOptions: { output: { comments: false } } })); @@ -19,7 +24,7 @@ const createConfig = options => { const filename = `./webpack/discord${process.env.VERSIONED === 'false' ? '' : '.' + version}${options.minify ? '.min' : ''}.js`; // eslint-disable-line return { - entry: './src/index.js', + entry: './browser.js', output: { path: __dirname, filename, From b9ea83afb442c4e74e02e4ab7a5700e3f975b92d Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 22 Aug 2017 12:20:26 -0700 Subject: [PATCH 0220/1359] update web.md (#1821) --- docs/topics/web.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/topics/web.md b/docs/topics/web.md index 0adb79526..660651bb7 100644 --- a/docs/topics/web.md +++ b/docs/topics/web.md @@ -1,9 +1,22 @@ # Web builds In addition to your usual Node applications, discord.js has special distributions available that are capable of running in web browsers. This is useful for client-side web apps that need to interact with the Discord API. -[Webpack 2](https://webpack.js.org/) is used to build these. +[Webpack 3](https://webpack.js.org/) is used to build these. -## Usage +## Restrictions +- Any voice-related functionality is unavailable, as there is currently no audio encoding/decoding capabilities without external native libraries, + which web browsers do not support. +- The ShardingManager cannot be used, since it relies on being able to spawn child processes for shards. +- None of the native optional packages are usable. + +### Require Library +If you are making your own webpack project, you can require `discord.js/browser` wherever you need to use discord.js, like so: +```js +const Discord = require('discord.js/browser'); +// do something with Discord like you normally would +``` + +### Webpack File You can obtain your desired version of discord.js' web build from the [webpack branch](https://github.com/hydrabolt/discord.js/tree/webpack) of the GitHub repository. There is a file for each branch and version of the library, and the ones ending in `.min.js` are minified to substantially reduce the size of the source code. @@ -15,13 +28,7 @@ Include the file on the page just as you would any other JS library, like so: Rather than importing discord.js with `require('discord.js')`, the entire `Discord` object is available as a global (on the `window`) object. The usage of the API isn't any different from using it in Node.js. -## Restrictions -- Any voice-related functionality is unavailable, as there is currently no audio encoding/decoding capabilities without external native libraries, - which web browsers do not support. -- The ShardingManager cannot be used, since it relies on being able to spawn child processes for shards. -- None of the optional packages are usable, since they're native libraries. - -## Example +#### Example ```html + + - + + From 270a278a6e93c65eeb4e861b230f61f5c3e1da5e Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 19 Jun 2018 21:16:05 +0200 Subject: [PATCH 0721/1359] feat(Guild): add support to edit `defaultMessageNotifications` level (#2592) * Added support for `defaultMessageNotifications` * Requested changes * docs: Change type for GuildEditData.defaultMessageNotifications --- src/structures/Guild.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index cbd5eacbd..61eae7a44 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -634,6 +634,7 @@ class Guild extends Base { * @property {Base64Resolvable} [icon] The icon of the guild * @property {GuildMemberResolvable} [owner] The owner of the guild * @property {Base64Resolvable} [splash] The splash screen of the guild + * @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications */ /** @@ -668,6 +669,11 @@ class Guild extends Base { if (typeof data.explicitContentFilter !== 'undefined') { _data.explicit_content_filter = Number(data.explicitContentFilter); } + if (typeof data.defaultMessageNotifications !== 'undefined') { + _data.default_message_notifications = typeof data.defaultMessageNotifications === 'string' ? + DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) : + Number(data.defaultMessageNotifications); + } return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } From 99a9a813d730e091de7c5f3a39df7125bcc76a58 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 20 Jun 2018 20:44:27 +0100 Subject: [PATCH 0722/1359] voice dep: bump prism-media to 0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb6cbf33d..d48bd0bc3 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "form-data": "^2.3.2", "node-fetch": "^2.1.2", "pako": "^1.0.0", - "prism-media": "^0.2.0", + "prism-media": "^0.3.0", "tweetnacl": "^1.0.0", "ws": "^4.0.0" }, From 594363a05eb13a327baeb45f3e4e17b8a075c725 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Wed, 20 Jun 2018 15:11:47 -0700 Subject: [PATCH 0723/1359] fix response parsing (#2614) * fix response parsing * rfc: better parseResponse function --- src/rest/handlers/RequestHandler.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js index c3812c92d..2c5655814 100644 --- a/src/rest/handlers/RequestHandler.js +++ b/src/rest/handlers/RequestHandler.js @@ -1,5 +1,11 @@ const DiscordAPIError = require('../DiscordAPIError'); -const { Events: { RATE_LIMIT } } = require('../../util/Constants'); +const { Events: { RATE_LIMIT }, browser } = require('../../util/Constants'); + +function parseResponse(res) { + if (res.headers.get('content-type').startsWith('application/json')) return res.json(); + if (browser) return res.blob(); + return res.buffer(); +} class RequestHandler { constructor(manager, handler) { @@ -77,7 +83,7 @@ class RequestHandler { } if (res.ok) { - res.json().then(item.resolve, item.reject); + parseResponse(res).then(item.resolve, item.reject); finish(); return; } @@ -95,7 +101,7 @@ class RequestHandler { finish(1e3 + this.client.options.restTimeOffset); } } else { - res.json().then(data => { + parseResponse(res).then(data => { item.reject(res.status >= 400 && res.status < 500 ? new DiscordAPIError(item.path, data, item.method) : res); }, item.reject); From 9997a67ab69a18ce5bf3a1e39bba2fc21c727d2b Mon Sep 17 00:00:00 2001 From: Camzure <31858081+Camzure@users.noreply.github.com> Date: Thu, 21 Jun 2018 15:35:52 -0400 Subject: [PATCH 0724/1359] Docs: Wording Improvements (#2607) * wording improvement * wording improvement for docs * docs: wording * wording * user account only: docs * Edited * Edited --- src/client/actions/MessageReactionAdd.js | 2 +- src/client/actions/MessageReactionRemove.js | 2 +- src/client/actions/MessageReactionRemoveAll.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 9d307ceee..0708d3380 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -30,7 +30,7 @@ class MessageReactionAdd extends Action { } /** - * Emitted whenever a reaction is added to a message. + * Emitted whenever a reaction is added to a cached message. * @event Client#messageReactionAdd * @param {MessageReaction} messageReaction The reaction object * @param {User} user The user that applied the guild or reaction emoji diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 983ba3f83..21e0dd02a 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -31,7 +31,7 @@ class MessageReactionRemove extends Action { } /** - * Emitted whenever a reaction is removed from a message. + * Emitted whenever a reaction is removed from a cached message. * @event Client#messageReactionRemove * @param {MessageReaction} messageReaction The reaction object * @param {User} user The user that removed the emoji or reaction emoji diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index 5aeb48c72..ac03c6e83 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -17,7 +17,7 @@ class MessageReactionRemoveAll extends Action { } /** - * Emitted whenever all reactions are removed from a message. + * Emitted whenever all reactions are removed from a cached message. * @event Client#messageReactionRemoveAll * @param {Message} message The message the reactions were removed from */ From 3d41748a3802aed678bddccf76c9e5d2baa48c1d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 21 Jun 2018 21:50:54 +0100 Subject: [PATCH 0725/1359] voice: fix not passing volume to transform constructor --- src/client/voice/player/BasePlayer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index 400f4bcef..a8889e44a 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -64,7 +64,7 @@ class BasePlayer extends EventEmitter { stream.pipe(opus); return this.playOpusStream(opus, options, streams); } - const volume = streams.volume = new prism.VolumeTransformer16LE(null, { volume: options ? options.volume : 1 }); + const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 }); stream.pipe(volume).pipe(opus); return this.playOpusStream(opus, options, streams); } @@ -75,7 +75,7 @@ class BasePlayer extends EventEmitter { if (options.volume !== false && !streams.input) { streams.input = stream; const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); - const volume = streams.volume = new prism.VolumeTransformer16LE(null, { volume: options ? options.volume : 1 }); + const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 }); streams.opus = stream .pipe(decoder) .pipe(volume) From 1f7c1ddaa21c6e58055b79f0a91a23aa136cd0fb Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 22 Jun 2018 17:42:04 +0100 Subject: [PATCH 0726/1359] voice: start update to v4, fix heartbeats --- src/client/voice/networking/VoiceWebSocket.js | 7 ++++--- src/util/Constants.js | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 4241aed21..08ee114a9 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -68,7 +68,7 @@ class VoiceWebSocket extends EventEmitter { * The actual WebSocket used to connect to the Voice WebSocket Server. * @type {WebSocket} */ - this.ws = WebSocket.create(`wss://${this.voiceConnection.authentication.endpoint}/`, { v: 3 }); + this.ws = WebSocket.create(`wss://${this.voiceConnection.authentication.endpoint}/`, { v: 4 }); this.ws.onopen = this.onOpen.bind(this); this.ws.onmessage = this.onMessage.bind(this); this.ws.onclose = this.onClose.bind(this); @@ -154,9 +154,10 @@ class VoiceWebSocket extends EventEmitter { */ onPacket(packet) { switch (packet.op) { + case VoiceOPCodes.HELLO: + this.setHeartbeat(packet.d.heartbeat_interval); + break; case VoiceOPCodes.READY: - // *.75 to correct for discord devs taking longer to fix things than i do to release versions - this.setHeartbeat(packet.d.heartbeat_interval * 0.75); /** * Emitted once the voice WebSocket receives the ready packet. * @param {Object} packet The received packet diff --git a/src/util/Constants.js b/src/util/Constants.js index 729448d7a..7a3c2bcca 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -197,6 +197,7 @@ exports.VoiceOPCodes = { HEARTBEAT: 3, SESSION_DESCRIPTION: 4, SPEAKING: 5, + HELLO: 8, }; exports.Events = { From 9296a30148883f67bfa249ddb729cccba6813aed Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 22 Jun 2018 17:47:00 +0100 Subject: [PATCH 0727/1359] voice: account for speaking now being a bitmask --- src/client/voice/VoiceConnection.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 5f674cf2f..d3b785cc3 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -137,7 +137,7 @@ class VoiceConnection extends EventEmitter { this.sockets.ws.sendPacket({ op: VoiceOPCodes.SPEAKING, d: { - speaking: this.speaking, + speaking: this.speaking ? 1 : 0, delay: 0, ssrc: this.authentication.ssrc, }, @@ -426,6 +426,7 @@ class VoiceConnection extends EventEmitter { * @private */ onSpeaking({ user_id, ssrc, speaking }) { + speaking = Boolean(speaking); const guild = this.channel.guild; const user = this.client.users.get(user_id); this.ssrcMap.set(+ssrc, user); From 08bbbe930144616338fc906ce5fc7a3e5725b670 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 22 Jun 2018 18:38:33 +0100 Subject: [PATCH 0728/1359] voice: handle new client_connect and client_disconnect packets --- src/client/voice/VoiceConnection.js | 2 +- src/client/voice/networking/VoiceWebSocket.js | 25 +++++++++++++------ src/client/voice/receiver/PacketHandler.js | 12 +++++---- src/util/Constants.js | 2 ++ 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index d3b785cc3..4c4701feb 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -429,7 +429,7 @@ class VoiceConnection extends EventEmitter { speaking = Boolean(speaking); const guild = this.channel.guild; const user = this.client.users.get(user_id); - this.ssrcMap.set(+ssrc, user); + this.ssrcMap.set(+ssrc, user_id); /** * Emitted whenever a user starts/stops speaking. * @event VoiceConnection#speaking diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 08ee114a9..087203e63 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -9,20 +9,20 @@ const WebSocket = require('../../../WebSocket'); * @private */ class VoiceWebSocket extends EventEmitter { - constructor(voiceConnection) { + constructor(connection) { super(); /** * The client of this voice WebSocket * @type {Client} */ - this.client = voiceConnection.voiceManager.client; + this.client = connection.voiceManager.client; /** * The Voice Connection that this WebSocket serves * @type {VoiceConnection} */ - this.voiceConnection = voiceConnection; + this.connection = connection; /** * How many connection attempts have been made @@ -32,7 +32,7 @@ class VoiceWebSocket extends EventEmitter { this.connect(); this.dead = false; - this.voiceConnection.on('closing', this.shutdown.bind(this)); + this.connection.on('closing', this.shutdown.bind(this)); } shutdown() { @@ -68,7 +68,7 @@ class VoiceWebSocket extends EventEmitter { * The actual WebSocket used to connect to the Voice WebSocket Server. * @type {WebSocket} */ - this.ws = WebSocket.create(`wss://${this.voiceConnection.authentication.endpoint}/`, { v: 4 }); + this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 4 }); this.ws.onopen = this.onOpen.bind(this); this.ws.onmessage = this.onMessage.bind(this); this.ws.onclose = this.onClose.bind(this); @@ -110,10 +110,10 @@ class VoiceWebSocket extends EventEmitter { this.sendPacket({ op: OPCodes.DISPATCH, d: { - server_id: this.voiceConnection.channel.guild.id, + server_id: this.connection.channel.guild.id, user_id: this.client.user.id, - token: this.voiceConnection.authentication.token, - session_id: this.voiceConnection.authentication.sessionID, + token: this.connection.authentication.token, + session_id: this.connection.authentication.sessionID, }, }).catch(() => { this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED')); @@ -177,6 +177,15 @@ class VoiceWebSocket extends EventEmitter { */ this.emit('sessionDescription', packet.d.mode, key); break; + case VoiceOPCodes.CLIENT_CONNECT: + this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id); + break; + case VoiceOPCodes.CLIENT_DISCONNECT: + for (const receiver of this.connection.receivers) { + const streamInfo = receiver.packets.streams.get(packet.d.user_id); + if (streamInfo) streamInfo.stream.push(null); + } + break; case VoiceOPCodes.SPEAKING: /** * Emitted whenever a speaking packet is received. diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 1931f06cf..03b5b6f69 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -11,11 +11,13 @@ class PacketHandler extends EventEmitter { this.streams = new Map(); } + get connection() { + return this.receiver.connection; + } + _stoppedSpeaking(userID) { - if (this.streams.has(userID)) { - const { stream, end } = this.streams.get(userID); - if (end === 'silence') stream.push(null); - } + const streamInfo = this.streams.get(userID); + if (streamInfo && streamInfo.end === 'silence') streamInfo.stream.push(null); } makeStream(user, end) { @@ -63,7 +65,7 @@ class PacketHandler extends EventEmitter { return packet; } - userFromSSRC(ssrc) { return this.receiver.connection.ssrcMap.get(ssrc); } + userFromSSRC(ssrc) { return this.connection.client.users.get(this.connection.ssrcMap.get(ssrc)); } push(buffer) { const ssrc = buffer.readUInt32BE(8); diff --git a/src/util/Constants.js b/src/util/Constants.js index 7a3c2bcca..79f95881b 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -198,6 +198,8 @@ exports.VoiceOPCodes = { SESSION_DESCRIPTION: 4, SPEAKING: 5, HELLO: 8, + CLIENT_CONNECT: 12, + CLIENT_DISCONNECT: 13, }; exports.Events = { From 18646b72f902d79b73b8010013df2cb9141a2755 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 22 Jun 2018 18:46:51 +0100 Subject: [PATCH 0729/1359] voice: use getters where possible --- src/client/voice/VoiceConnection.js | 14 ++++++++------ src/client/voice/networking/VoiceWebSocket.js | 15 ++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 4c4701feb..324b113a7 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -36,12 +36,6 @@ class VoiceConnection extends EventEmitter { */ this.voiceManager = voiceManager; - /** - * The client that instantiated this connection - * @type {Client} - */ - this.client = voiceManager.client; - /** * The voice channel this connection is currently serving * @type {VoiceChannel} @@ -116,6 +110,14 @@ class VoiceConnection extends EventEmitter { this.authenticate(); } + /** + * The client that instantiated this connection + * @type {Client} + */ + get client() { + return this.voiceManager.client; + } + /** * The current stream dispatcher (if any) * @type {?StreamDispatcher} diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 087203e63..1e08dedf9 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -11,13 +11,6 @@ const WebSocket = require('../../../WebSocket'); class VoiceWebSocket extends EventEmitter { constructor(connection) { super(); - - /** - * The client of this voice WebSocket - * @type {Client} - */ - this.client = connection.voiceManager.client; - /** * The Voice Connection that this WebSocket serves * @type {VoiceConnection} @@ -35,6 +28,14 @@ class VoiceWebSocket extends EventEmitter { this.connection.on('closing', this.shutdown.bind(this)); } + /** + * The client of this voice WebSocket + * @type {Client} + */ + get client() { + return this.connection.voiceManager.client; + } + shutdown() { this.dead = true; this.reset(); From bf738b34de1f02f83b4ddb3f60f3947840db91e7 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 23 Jun 2018 16:20:59 +0300 Subject: [PATCH 0730/1359] fix(BasePlayer): incorrectly mutating FFMPEG_ARGUMENTS when using seek (#2622) Because args was not a copy of FFMPEG_ARGIMENTS, but a reference to it, pushing 'ss' argument pushed it to FFMPEG_ARGUMENTS, making it persistent. --- src/client/voice/player/BasePlayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index a8889e44a..e19984dff 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -45,7 +45,7 @@ class BasePlayer extends EventEmitter { const isStream = input instanceof ReadableStream; - const args = isStream ? FFMPEG_ARGUMENTS : ['-i', input, ...FFMPEG_ARGUMENTS]; + const args = isStream ? FFMPEG_ARGUMENTS.slice() : ['-i', input, ...FFMPEG_ARGUMENTS]; if (options.seek) args.push('-ss', String(options.seek)); const ffmpeg = new prism.FFmpeg({ args }); From de408d735b7f6aa56052e903b95b64a06350a3bd Mon Sep 17 00:00:00 2001 From: Souji Date: Sat, 23 Jun 2018 15:22:57 +0200 Subject: [PATCH 0731/1359] docs(Guild): memberCount is not as of ready (#2621) but updated every guild member add or remove --- 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 61eae7a44..62615e3a4 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -107,7 +107,7 @@ class Guild extends Base { this.region = data.region; /** - * The full amount of members in this guild as of `READY` + * The full amount of members in this guild * @type {number} */ this.memberCount = data.member_count || this.memberCount; From 58e5017159ebe9b89327b1647cb433a412be2a46 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 23 Jun 2018 16:08:40 -0500 Subject: [PATCH 0732/1359] fix: ratelimits (#2615) * Add timeDifference calcs back into ratelimits And fix x-ratelimit-reset is in seconds, not ms: https://puu.sh/AIXxY/9b3989b248.png * mutate reset time with date header instead * fix defaulting of reset and remaining if the reset header is not available, then the reset time should be Date.now() instead of the difference between the date header and Date.now() If the date header is null, fall back to Date.now() since it's the best we can do (this should never happen, but safer to handle it just in case) if remaining is 0 we don't want to default it back to 1 since 0 is falsy --- src/rest/RESTManager.js | 10 ---------- src/rest/handlers/RequestHandler.js | 10 +++++++--- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 586125a54..cf6227a8a 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -12,7 +12,6 @@ class RESTManager { this.globallyRateLimited = false; this.tokenPrefix = tokenPrefix; this.versioned = true; - this.timeDifferences = []; if (client.options.restSweepInterval > 0) { client.setInterval(() => { this.handlers.sweep(handler => handler._inactive); @@ -24,15 +23,6 @@ class RESTManager { return routeBuilder(this); } - get timeDifference() { - return Math.round(this.timeDifferences.reduce((a, b) => a + b, 0) / this.timeDifferences.length); - } - - set timeDifference(ms) { - this.timeDifferences.unshift(ms); - if (this.timeDifferences.length > 5) this.timeDifferences.length = 5; - } - getAuth() { const token = this.client.token || this.client.accessToken; const prefixed = !!this.client.application || (this.client.user && this.client.user.bot); diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js index 2c5655814..03e5e8fca 100644 --- a/src/rest/handlers/RequestHandler.js +++ b/src/rest/handlers/RequestHandler.js @@ -29,7 +29,7 @@ class RequestHandler { } get _inactive() { - return this.queue.length === 0 && !this.limited && Date.now() > this.resetTime && this.busy !== true; + return this.queue.length === 0 && !this.limited && this.busy !== true; } /* eslint-disable prefer-promise-reject-errors */ @@ -78,8 +78,12 @@ class RequestHandler { if (res && res.headers) { if (res.headers.get('x-ratelimit-global')) this.manager.globallyRateLimited = true; this.limit = Number(res.headers.get('x-ratelimit-limit') || Infinity); - this.resetTime = Number(res.headers.get('x-ratelimit-reset') || 0); - this.remaining = Number(res.headers.get('x-ratelimit-remaining') || 1); + const reset = res.headers.get('x-ratelimit-reset'); + this.resetTime = reset !== null ? + (Number(reset) * 1e3) - new Date(res.headers.get('date') || Date.now()).getTime() + Date.now() : + Date.now(); + const remaining = res.headers.get('x-ratelimit-remaining'); + this.remaining = remaining !== null ? Number(remaining) : 1; } if (res.ok) { From bfbb723f4293c883172d977e4451104b8cfe42b7 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sat, 23 Jun 2018 16:09:03 -0500 Subject: [PATCH 0733/1359] feat(ShardingManager): add execArgv option (#2585) --- src/sharding/Shard.js | 15 +++++++++++---- src/sharding/ShardingManager.js | 10 +++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 1f3f56455..44a9f970b 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -14,9 +14,8 @@ 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 = []) { + constructor(manager, id) { super(); /** @@ -35,7 +34,13 @@ class Shard extends EventEmitter { * Arguments for the shard's process * @type {string[]} */ - this.args = args; + this.args = manager.args; + + /** + * Arguments for the shard's process executable + * @type {string[]} + */ + this.execArgv = manager.execArgv; /** * Environment variables for the shard's process @@ -91,7 +96,9 @@ class Shard extends EventEmitter { 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 }) + this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { + env: this.env, execArgv: this.execArgv, + }) .on('message', this._handleMessage.bind(this)) .on('exit', this._exitListener); diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 5283c9414..1af2b6632 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -22,6 +22,7 @@ class ShardingManager extends EventEmitter { * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto" * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting * @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning + * @param {string[]} [options.execArgv=[]] Arguments to pass to the shard script executable when spawning * @param {string} [options.token] Token to use for automatic shard count and passing to shards */ constructor(file, options = {}) { @@ -30,6 +31,7 @@ class ShardingManager extends EventEmitter { totalShards: 'auto', respawn: true, shardArgs: [], + execArgv: [], token: null, }, options); @@ -70,6 +72,12 @@ class ShardingManager extends EventEmitter { */ this.shardArgs = options.shardArgs; + /** + * An array of arguments to pass to the executable + * @type {string[]} + */ + this.execArgv = options.execArgv; + /** * Token to use for obtaining the automatic shard count, and passing to shards * @type {?string} @@ -90,7 +98,7 @@ class ShardingManager extends EventEmitter { * @returns {Shard} */ createShard(id = this.shards.size) { - const shard = new Shard(this, id, this.shardArgs); + const shard = new Shard(this, id); this.shards.set(id, shard); /** * Emitted upon creating a shard. From ac0c15f7f1950eaaebfa29a741ada64d67a086a5 Mon Sep 17 00:00:00 2001 From: FireController1847 Date: Sun, 24 Jun 2018 07:47:54 -0600 Subject: [PATCH 0734/1359] Change ytdl example (#2619) --- src/client/voice/util/PlayInterface.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 17318253b..2a3712f49 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -47,7 +47,7 @@ class PlayInterface { * connection.play('/home/hydrabolt/audio.mp3', { volume: 0.5 }); * @example * // Play a ReadableStream - * connection.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { filter: 'audioonly' })); + * connection.play(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { quality: 'highestaudio' })); * @example * // Play a voice broadcast * const broadcast = client.createVoiceBroadcast(); From 6b3bfdd7dede049ba592e94ef1b1acc40a5e37fe Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 24 Jun 2018 11:22:57 -0500 Subject: [PATCH 0735/1359] fix(Shard): fix default args --- src/sharding/Shard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 44a9f970b..e5d50b19f 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -34,13 +34,13 @@ class Shard extends EventEmitter { * Arguments for the shard's process * @type {string[]} */ - this.args = manager.args; + this.args = manager.shardArgs || []; /** * Arguments for the shard's process executable * @type {string[]} */ - this.execArgv = manager.execArgv; + this.execArgv = manager.execArgv || []; /** * Environment variables for the shard's process From f711aa35ac8556f7725430c297bf6b9e5eec5acd Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 26 Jun 2018 12:07:09 -0500 Subject: [PATCH 0736/1359] fix: typo in src/errors/messages.js (#2631) fixes #2630 --- src/errors/Messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 0485296a6..4e362635f 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -12,7 +12,7 @@ const Messages = { WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, - PERMISSION_INVALID: 'Invalid permission string or number.', + PERMISSIONS_INVALID: 'Invalid permission string or number.', RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.', From bcdb8011e350ac57e57615d11659243fcb71fddd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Tue, 26 Jun 2018 23:14:54 +0200 Subject: [PATCH 0737/1359] build(devDeps): update eslint requirement to ^5.0.1 (#2629) * build(devDeps): update eslint requirement to ^5.0.1 Updates the requirements on [eslint](https://github.com/eslint/eslint) to permit the latest version. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/commits/v5.0.1) Signed-off-by: dependabot[bot] * fix(eslint): disable getter-return rule --- .eslintrc.json | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 979a9acc1..376e9f70d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -80,6 +80,7 @@ "no-undef-init": "error", "callback-return": "error", + "getter-return": "off", "handle-callback-err": "error", "no-mixed-requires": "error", "no-new-require": "error", diff --git a/package.json b/package.json index d48bd0bc3..bdd4fdd9f 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "devDependencies": { "@types/node": "^10.0.3", "discord.js-docgen": "discordjs/docgen", - "eslint": "^4.17.0", + "eslint": "^5.0.1", "json-filter-loader": "^1.0.0", "uglifyjs-webpack-plugin": "^1.1.8", "webpack": "^4.5.0", From 41a1dee53360dbc3c358c7ada296c9311e324778 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Fri, 29 Jun 2018 13:02:38 -0500 Subject: [PATCH 0738/1359] fix(Util): circular reference --- src/util/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index 66117e9e3..ea99f5328 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1,4 +1,3 @@ -const Collection = require('./Collection'); const { Colors, DefaultOptions, Endpoints } = require('./Constants'); const fetch = require('node-fetch'); const { Error: DiscordError, RangeError, TypeError } = require('../errors'); @@ -403,6 +402,7 @@ class Util { */ /* eslint-disable func-names */ static mixin(store, ignored) { + const Collection = require('./Collection'); Object.getOwnPropertyNames(Collection.prototype) .concat(Object.getOwnPropertyNames(Map.prototype)).forEach(prop => { if (ignored.includes(prop)) return; From 92e2c3c7fd88d9e5b708920bc16e247395813021 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Fri, 29 Jun 2018 13:18:13 -0500 Subject: [PATCH 0739/1359] fix: instantiate constructors before using those properties --- src/client/websocket/WebSocketConnection.js | 6 ++++++ src/rest/handlers/RequestHandler.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index 5e07b1a69..fdf58eb5c 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -44,6 +44,12 @@ class WebSocketConnection extends EventEmitter { */ this.sequence = -1; + /** + * The current sessionID of the WebSocket + * @type {string} + */ + this.sessionID = null; + /** * The current status of the client * @type {number} diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js index 03e5e8fca..095699e0d 100644 --- a/src/rest/handlers/RequestHandler.js +++ b/src/rest/handlers/RequestHandler.js @@ -15,7 +15,7 @@ class RequestHandler { this.limit = Infinity; this.resetTime = null; this.remaining = 1; - + this.busy = false; this.queue = []; } From 93f8c8547b1033b10a0204a8e7343e64af34120a Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Fri, 29 Jun 2018 13:59:28 -0500 Subject: [PATCH 0740/1359] fix(Shard): allow node to default to process.execArgv --- src/sharding/Shard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index e5d50b19f..a7aa868cb 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -38,9 +38,9 @@ class Shard extends EventEmitter { /** * Arguments for the shard's process executable - * @type {string[]} + * @type {?string[]} */ - this.execArgv = manager.execArgv || []; + this.execArgv = manager.execArgv; /** * Environment variables for the shard's process From 2694c0d442a008a1e40c17df147a22bc9534049a Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Fri, 29 Jun 2018 14:26:03 -0500 Subject: [PATCH 0741/1359] fix(RequestHandler): provide proper route and method --- src/rest/handlers/RequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js index 095699e0d..ce0456e1d 100644 --- a/src/rest/handlers/RequestHandler.js +++ b/src/rest/handlers/RequestHandler.js @@ -107,7 +107,7 @@ class RequestHandler { } else { parseResponse(res).then(data => { item.reject(res.status >= 400 && res.status < 500 ? - new DiscordAPIError(item.path, data, item.method) : res); + new DiscordAPIError(item.request.route, data, item.request.method) : res); }, item.reject); finish(); } From 938d87d5d630f533e17c6dfd96f1f6622ad87079 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 1 Jul 2018 10:17:20 -0500 Subject: [PATCH 0742/1359] fix(Shard): actually remove default execArgv --- src/sharding/ShardingManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 1af2b6632..a98c46bd8 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -31,7 +31,6 @@ class ShardingManager extends EventEmitter { totalShards: 'auto', respawn: true, shardArgs: [], - execArgv: [], token: null, }, options); From 077d41fbca258b5e4c96c7ca08edeb15dcd066f5 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 1 Jul 2018 10:19:18 -0500 Subject: [PATCH 0743/1359] fix(Webhook): import Channel correctly --- 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 e37d42302..cdb396c07 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,5 +1,5 @@ const DataResolver = require('../util/DataResolver'); -const { Channel } = require('./Channel'); +const Channel = require('./Channel'); const { createMessage } = require('./shared'); /** From 95b2dd3fe63ec9b6c7cec4f63f8276f4d907f228 Mon Sep 17 00:00:00 2001 From: Souji Date: Sun, 1 Jul 2018 18:03:47 +0200 Subject: [PATCH 0744/1359] feat: Add support for Guild#setDefaultMessageNotifications (#2624) * Feat: Guild#setDefaultMessageNotifications * fix typo --- src/structures/Guild.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 62615e3a4..17ceaba89 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -688,6 +688,18 @@ class Guild extends Base { return this.edit({ explicitContentFilter }, reason); } + /* eslint-disable max-len */ + /** + * Edits the setting of the default message notifications of the guild. + * @param {DefaultMessageNotifications|number} defaultMessageNotifications The new setting for the default message notifications + * @param {string} [reason] Reason for changing the setting of the default message notifications + * @returns {Promise} + */ + setDefaultMessageNotifications(defaultMessageNotifications, reason) { + return this.edit({ defaultMessageNotifications }, reason); + } + /* eslint-enable max-len */ + /** * Edits the name of the guild. * @param {string} name The new name of the guild From 526832058e78de8d8936e8db61b60775a0bcf3a5 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 8 Jul 2018 21:12:30 +0200 Subject: [PATCH 0745/1359] fix(Message): keep reply option when using an embed or attachment See #2651 --- src/structures/shared/CreateMessage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js index 55d779cf5..ee9756f77 100644 --- a/src/structures/shared/CreateMessage.js +++ b/src/structures/shared/CreateMessage.js @@ -20,7 +20,7 @@ module.exports = async function createMessage(channel, options) { if (isNaN(options.nonce) || options.nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); } - let { content } = options; + let { content, reply } = options; if (options instanceof MessageEmbed) options = webhook ? { embeds: [options] } : { embed: options }; if (options instanceof MessageAttachment) options = { files: [options.file] }; @@ -36,9 +36,9 @@ module.exports = async function createMessage(channel, options) { } } - 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 (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 (options.split) options.split.prepend = `${mention}, ${options.split.prepend || ''}`; content = `${mention}${typeof options.content !== 'undefined' ? `, ${options.content}` : ''}`; } From f67d682223a0b0d330ec34738a3dfe51a50f3d8f Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Tue, 10 Jul 2018 21:39:58 -0500 Subject: [PATCH 0746/1359] fix(ShardingManager): respawnAll shard iteration --- src/sharding/ShardingManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index a98c46bd8..2b96ab2dd 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -194,7 +194,7 @@ class ShardingManager extends EventEmitter { */ async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { let s = 0; - for (const shard of this.shards) { + for (const shard of this.shards.values()) { const promises = [shard.respawn(respawnDelay, waitForReady)]; if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop From 7d2744be89f90fa16b77b6fbad95902e26f4564a Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 12 Jul 2018 16:11:04 +0100 Subject: [PATCH 0747/1359] voice: use development version of prism --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bdd4fdd9f..fe26eb93d 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "form-data": "^2.3.2", "node-fetch": "^2.1.2", "pako": "^1.0.0", - "prism-media": "^0.3.0", + "prism-media": "hydrabolt/prism-media", "tweetnacl": "^1.0.0", "ws": "^4.0.0" }, From 3d25277839c8cb9c1c1db2f2ed7226d266a93ed1 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 15 Jul 2018 08:42:40 +0200 Subject: [PATCH 0748/1359] chore: update typings submodule --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index eba781a96..b1127ec10 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit eba781a963a54b56d633e71a1b1b91b5f683be3c +Subproject commit b1127ec10ee951158dcc7220fe37a6e005806a1d From e0378cf3505f3bc878ba824882d63ea6fa2261d7 Mon Sep 17 00:00:00 2001 From: Kyra Date: Wed, 18 Jul 2018 11:37:57 +0200 Subject: [PATCH 0749/1359] refactor(Webhook): make token not enumerable (#2566) * Hide Webhook#token (consistency with Client#token) * Make `Webhook#token` writable * fix: devsnek's requested change Webhook#token must be both writable and configurable. --- 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 cdb396c07..088806ca2 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -28,7 +28,7 @@ class Webhook { * The token for the webhook * @type {string} */ - this.token = data.token; + Object.defineProperty(this, 'token', { value: data.token, writable: true, configurable: true }); /** * The avatar for the webhook From 36806612bfb6af48a99a4dfff45a9823d4d391bd Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 18 Jul 2018 11:39:34 +0200 Subject: [PATCH 0750/1359] fix(Invite): support for group dm invites (#2603) * fix(Invite): support group dm invites * refactor(Invite): mark all optional properties as nullable and default them to null --- src/structures/Invite.js | 64 +++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index f03221972..87e590a57 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -3,7 +3,7 @@ const Base = require('./Base'); /** * Represents an invitation to a guild channel. - * The only guaranteed properties are `code`, `guild` and `channel`. Other properties can be missing. + * The only guaranteed properties are `code` and `channel`. Other properties can be missing. * @extends {Base} */ class Invite extends Base { @@ -15,9 +15,9 @@ class Invite extends Base { _patch(data) { /** * The guild the invite is for - * @type {Guild} + * @type {?Guild} */ - this.guild = this.client.guilds.add(data.guild, false); + this.guild = data.guild ? this.client.guilds.add(data.guild, false) : null; /** * The code for this invite @@ -27,59 +27,57 @@ class Invite extends Base { /** * The approximate number of online members of the guild this invite is for - * @type {number} + * @type {?number} */ - this.presenceCount = data.approximate_presence_count; + this.presenceCount = data.approximate_presence_count || null; /** * The approximate total number of members of the guild this invite is for - * @type {number} + * @type {?number} */ - this.memberCount = data.approximate_member_count; + this.memberCount = data.approximate_member_count || null; /** * The number of text channels the guild this invite goes to has - * @type {number} + * @type {?number} */ - this.textChannelCount = data.guild.text_channel_count; + this.textChannelCount = data.guild ? data.guild.text_channel_count : null; /** * The number of voice channels the guild this invite goes to has - * @type {number} + * @type {?number} */ - this.voiceChannelCount = data.guild.voice_channel_count; + this.voiceChannelCount = data.guild ? data.guild.voice_channel_count : null; /** * Whether or not this invite is temporary - * @type {boolean} + * @type {?boolean} */ - this.temporary = data.temporary; + this.temporary = data.temporary || null; /** * The maximum age of the invite, in seconds * @type {?number} */ - this.maxAge = data.max_age; + this.maxAge = data.max_age || null; /** * How many times this invite has been used - * @type {number} + * @type {?number} */ - this.uses = data.uses; + this.uses = data.uses || null; /** * The maximum uses of this invite - * @type {number} + * @type {?number} */ - this.maxUses = data.max_uses; + this.maxUses = data.max_uses || null; - if (data.inviter) { - /** - * The user who created this invite - * @type {?User} - */ - this.inviter = this.client.users.add(data.inviter); - } + /** + * The user who created this invite + * @type {?User} + */ + this.inviter = data.inviter ? this.client.users.add(data.inviter) : null; /** * The channel the invite is for @@ -89,36 +87,36 @@ class Invite extends Base { /** * The timestamp the invite was created at - * @type {number} + * @type {?number} */ - this.createdTimestamp = new Date(data.created_at).getTime(); + this.createdTimestamp = new Date(data.created_at).getTime() || null; } /** * The time the invite was created at - * @type {Date} + * @type {?Date} * @readonly */ get createdAt() { - return new Date(this.createdTimestamp); + return this.createdTimestamp ? new Date(this.createdTimestamp) : null; } /** * The timestamp the invite will expire at - * @type {number} + * @type {?number} * @readonly */ get expiresTimestamp() { - return this.createdTimestamp + (this.maxAge * 1000); + return this.createdTimestamp ? this.createdTimestamp + (this.maxAge * 1000) : null; } /** * The time the invite will expire at - * @type {Date} + * @type {?Date} * @readonly */ get expiresAt() { - return new Date(this.expiresTimestamp); + return this.expiresTimestamp ? new Date(this.expiresTimestamp) : null; } /** From 717e7f094a0b30a88ec48b073373223861e87b12 Mon Sep 17 00:00:00 2001 From: Gymnophoria Date: Wed, 18 Jul 2018 02:37:36 -0700 Subject: [PATCH 0751/1359] docs(Client): clarify messageReactionRemove event's user description (#2657) * Clarify messageReactionRemove user description * Update MessageReactionRemove.js * wait one more word difference lol --- src/client/actions/MessageReactionRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 21e0dd02a..63e349f7d 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -34,7 +34,7 @@ class MessageReactionRemove extends Action { * Emitted whenever a reaction is removed from a cached message. * @event Client#messageReactionRemove * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user that removed the emoji or reaction emoji + * @param {User} user The user whose emoji or reaction emoji was removed */ module.exports = MessageReactionRemove; From 3a7a7d730bcedf4a11789467f9d436d46949c9d2 Mon Sep 17 00:00:00 2001 From: Isabella Date: Mon, 23 Jul 2018 16:32:51 -0400 Subject: [PATCH 0752/1359] fix(MessageReaction): only delete reaction if cache and count is empty (#2661) --- src/structures/MessageReaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 872c7a827..faf3bfbfb 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -71,7 +71,7 @@ class MessageReaction { this.users.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; - if (this.count <= 0) { + if (this.count <= 0 && this.users.size === 0) { this.message.reactions.remove(this.emoji.id || this.emoji.name); } } From c46cbbfd8464714dbea0ed4364821ac7efbbccfe Mon Sep 17 00:00:00 2001 From: PLASMAchicken Date: Thu, 26 Jul 2018 03:04:47 +0200 Subject: [PATCH 0753/1359] docs: added sizes 16, 32, 64 (#2666) * documented sizes 16, 32, 64 * Removed space * Added typedef * The options to provide => Options for the Image URL * fixed --- src/structures/ClientApplication.js | 8 ++------ src/structures/GroupDMChannel.js | 4 +--- src/structures/Guild.js | 8 ++------ src/structures/User.js | 10 ++-------- src/util/Constants.js | 7 +++++++ 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index f76e98b0a..b884dfd11 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -121,9 +121,7 @@ class ClientApplication extends Base { /** * A link to the application's icon. - * @param {Object} [options={}] Options for the icon url - * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` - * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} URL to the icon */ iconURL({ format, size } = {}) { @@ -133,9 +131,7 @@ class ClientApplication extends Base { /** * A link to this application's cover image. - * @param {Object} [options={}] Options for the cover image url - * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` - * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} URL to the cover image */ coverImage({ format, size } = {}) { diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 14ceeae5d..d67d33ad2 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -116,9 +116,7 @@ class GroupDMChannel extends Channel { /** * Gets the URL to this Group DM's icon. - * @param {Object} [options={}] Options for the icon url - * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` - * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ iconURL({ format, size } = {}) { diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 17ceaba89..812250630 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -288,9 +288,7 @@ class Guild extends Base { /** * The URL to this guild's icon. - * @param {Object} [options={}] Options for the icon url - * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` - * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ iconURL({ format, size } = {}) { @@ -309,9 +307,7 @@ class Guild extends Base { /** * The URL to this guild's splash. - * @param {Object} [options={}] Options for the splash url - * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg` - * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ splashURL({ format, size } = {}) { diff --git a/src/structures/User.js b/src/structures/User.js index da36de7bb..e020d288a 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -115,10 +115,7 @@ class User extends Base { /** * A link to the user's avatar. - * @param {Object} [options={}] Options for the avatar url - * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, - * it will be `gif` for animated avatars or otherwise `webp` - * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ avatarURL({ format, size } = {}) { @@ -138,10 +135,7 @@ class User extends Base { /** * A link to the user's avatar if they have one. * Otherwise a link to their default avatar will be returned. - * @param {Object} [options={}] Options for the avatar url - * @param {string} [options.format='webp'] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, - * it will be `gif` for animated avatars or otherwise `webp` - * @param {number} [options.size=128] One of `128`, `256`, `512`, `1024`, `2048` + * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {string} */ displayAvatarURL(options) { diff --git a/src/util/Constants.js b/src/util/Constants.js index 79f95881b..4c381e4d6 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -111,6 +111,13 @@ function makeImageUrl(root, { format = 'webp', size } = {}) { if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size); return `${root}.${format}${size ? `?size=${size}` : ''}`; } +/** + * Options for Image URLs. + * @typedef {Object} ImageURLOptions + * @property {string} [format] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, + * it will be `gif` for animated avatars or otherwise `webp` + * @property {number} [size] One of `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048` + */ exports.Endpoints = { CDN(root) { From f963621ef1ad952bfe38f42a65c9025967f3bf50 Mon Sep 17 00:00:00 2001 From: Yukine Date: Thu, 26 Jul 2018 03:11:38 +0200 Subject: [PATCH 0754/1359] fix(GuildEmoji): added a check for managed emojis in fetchAuthor (#2645) --- src/errors/Messages.js | 1 + src/structures/GuildEmoji.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 4e362635f..6905cbbc5 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -94,6 +94,7 @@ const Messages = { WEBHOOK_MESSAGE: 'The message was not sent by a webhook.', EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji', + EMOJI_MANAGED: 'Emoji is managed and has no Author.', REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.', }; diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index e8027ab1c..4811f207e 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,5 +1,6 @@ const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore'); const Permissions = require('../util/Permissions'); +const { Error } = require('../errors'); const Snowflake = require('../util/Snowflake'); const Emoji = require('./Emoji'); @@ -87,6 +88,9 @@ class GuildEmoji extends Emoji { * @returns {Promise} */ fetchAuthor() { + if (this.managed) { + return Promise.reject(new Error('EMOJI_MANAGED')); + } return this.client.api.guilds(this.guild.id).emojis(this.id).get() .then(emoji => this.client.users.add(emoji.user)); } From 5afd77ab730f413d0090be6ce0b0a7083ef15f7c Mon Sep 17 00:00:00 2001 From: bdistin Date: Wed, 25 Jul 2018 21:14:23 -0500 Subject: [PATCH 0755/1359] refactor: remove user bot methods (#2559) * [WIP] Remove user bots * more backend userbot removal * Add mfaEnabled back * revert client presences store removal * partially revert getAuth changes * remove more no longer used children of ClientUserGuildSettings * fix a bug with this pr and TextBasedChannel.applyToClass * remove a syncGuilds reference * more user bot data handling * various guildSync cleanup * bots can't call logout Had the user/bot portions of the code mixed up. Though, does this need to be a promise anymore? * make ClientManager#destroy() sync It nolonger needs to be a promise, and nothing depended on it being a promise that I can tell. * requested change * Fix massive error * no longer used as it's userbot only --- src/client/Client.js | 37 +--- src/client/ClientManager.js | 10 +- src/client/actions/ActionsManager.js | 2 - src/client/actions/GuildSync.js | 29 ---- src/client/actions/UserNoteUpdate.js | 30 ---- .../packets/WebSocketPacketManager.js | 6 - .../websocket/packets/handlers/GuildSync.js | 11 -- .../websocket/packets/handlers/Ready.js | 23 --- .../packets/handlers/RelationshipAdd.js | 19 --- .../packets/handlers/RelationshipRemove.js | 19 --- .../handlers/UserGuildSettingsUpdate.js | 21 --- .../packets/handlers/UserNoteUpdate.js | 12 -- .../packets/handlers/UserSettingsUpdate.js | 18 -- src/errors/Messages.js | 2 - src/index.js | 3 - src/rest/RESTManager.js | 2 +- src/stores/GuildStore.js | 2 +- src/stores/MessageStore.js | 7 - src/stores/UserStore.js | 1 - src/structures/ClientApplication.js | 20 --- src/structures/ClientUser.js | 158 +----------------- src/structures/ClientUserChannelOverride.js | 28 ---- src/structures/ClientUserGuildSettings.js | 60 ------- src/structures/ClientUserSettings.js | 80 --------- src/structures/GroupDMChannel.js | 8 +- src/structures/Guild.js | 143 ---------------- src/structures/GuildChannel.js | 32 ---- src/structures/Message.js | 14 -- src/structures/User.js | 31 ---- src/structures/UserProfile.js | 83 --------- src/structures/interfaces/TextBasedChannel.js | 35 ---- src/structures/shared/Search.js | 100 ----------- src/structures/shared/index.js | 1 - src/util/Constants.js | 71 -------- 34 files changed, 15 insertions(+), 1103 deletions(-) delete mode 100644 src/client/actions/GuildSync.js delete mode 100644 src/client/actions/UserNoteUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildSync.js delete mode 100644 src/client/websocket/packets/handlers/RelationshipAdd.js delete mode 100644 src/client/websocket/packets/handlers/RelationshipRemove.js delete mode 100644 src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js delete mode 100644 src/client/websocket/packets/handlers/UserNoteUpdate.js delete mode 100644 src/client/websocket/packets/handlers/UserSettingsUpdate.js delete mode 100644 src/structures/ClientUserChannelOverride.js delete mode 100644 src/structures/ClientUserGuildSettings.js delete mode 100644 src/structures/ClientUserSettings.js delete mode 100644 src/structures/UserProfile.js delete mode 100644 src/structures/shared/Search.js diff --git a/src/client/Client.js b/src/client/Client.js index 0d1900a8b..5b03c4435 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -97,8 +97,7 @@ class Client extends BaseClient { this.channels = new ChannelStore(this); /** - * Presences that have been received for the client user's friends, mapped by user IDs - * This is only filled when using a user account. + * Presences that have been received for the client user, mapped by user IDs * @type {ClientPresenceStore} */ this.presences = new ClientPresenceStore(this); @@ -106,7 +105,7 @@ class Client extends BaseClient { Object.defineProperty(this, 'token', { writable: true }); if (!browser && !this.token && 'CLIENT_TOKEN' in process.env) { /** - * Authorization token for the logged in user/bot + * Authorization token for the logged in bot * This should be kept private at all times. * @type {?string} */ @@ -240,10 +239,6 @@ class Client extends BaseClient { /** * Logs the client in, establishing a websocket connection to Discord. - * Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever - * possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts. - * Bot accounts also have access to many features that user accounts cannot utilise. User accounts that are found to - * be abusing/overusing the API will be banned, locking you out of Discord entirely. * @param {string} token Token of the account to log in with * @returns {Promise} Token of the account used * @example @@ -262,27 +257,13 @@ class Client extends BaseClient { /** * Logs out, terminates the connection to Discord, and destroys the client. - * @returns {Promise} + * @returns {void} */ destroy() { super.destroy(); return this.manager.destroy(); } - /** - * Requests a sync of guild data with Discord. - * This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}. - * This is only available when using a user account. - * @param {Guild[]|Collection} [guilds=this.guilds] An array or collection of guilds to sync - */ - syncGuilds(guilds = this.guilds) { - if (this.user.bot) return; - this.ws.send({ - op: 12, - d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id), - }); - } - /** * Obtains an invite from Discord. * @param {InviteResolvable} invite Invite code or URL @@ -369,22 +350,16 @@ class Client extends BaseClient { } /** - * Obtains the OAuth Application of the bot from Discord. - * @param {Snowflake} [id='@me'] ID of application to fetch + * Obtains the OAuth Application of this bot from Discord. * @returns {Promise} - * @example - * client.fetchApplication('id') - * .then(application => console.log(`Obtained application with name: ${application.name}`) - * .catch(console.error); */ - fetchApplication(id = '@me') { - return this.api.oauth2.applications(id).get() + fetchApplication() { + return this.api.oauth2.applications('@me').get() .then(app => new ClientApplication(this, app)); } /** * Generates a link that can be used to invite the bot to a guild. - * This is only available when using a bot account. * @param {PermissionResolvable} [permissions] Permissions to request * @returns {Promise} * @example diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 2a081e3a7..b283fd780 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -63,15 +63,7 @@ class ClientManager { destroy() { this.client.ws.destroy(); - if (!this.client.user) return Promise.resolve(); - if (this.client.user.bot) { - this.client.token = null; - return Promise.resolve(); - } else { - return this.client.api.logout.post().then(() => { - this.client.token = null; - }); - } + if (this.client.user) this.client.token = null; } } diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 9708d17b2..9e44f4260 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -20,8 +20,6 @@ class ActionsManager { this.register(require('./GuildRoleDelete')); this.register(require('./GuildRoleUpdate')); this.register(require('./UserUpdate')); - this.register(require('./UserNoteUpdate')); - this.register(require('./GuildSync')); this.register(require('./GuildEmojiCreate')); this.register(require('./GuildEmojiDelete')); this.register(require('./GuildEmojiUpdate')); diff --git a/src/client/actions/GuildSync.js b/src/client/actions/GuildSync.js deleted file mode 100644 index f7dbde6ad..000000000 --- a/src/client/actions/GuildSync.js +++ /dev/null @@ -1,29 +0,0 @@ -const Action = require('./Action'); - -class GuildSync extends Action { - handle(data) { - const client = this.client; - - const guild = client.guilds.get(data.id); - if (guild) { - if (data.presences) { - for (const presence of data.presences) guild.presences.add(presence); - } - - if (data.members) { - for (const syncMember of data.members) { - const member = guild.members.get(syncMember.user.id); - if (member) { - member._patch(syncMember); - } else { - guild.members.add(syncMember, false); - } - } - } - - if ('large' in data) guild.large = data.large; - } - } -} - -module.exports = GuildSync; diff --git a/src/client/actions/UserNoteUpdate.js b/src/client/actions/UserNoteUpdate.js deleted file mode 100644 index 77bc14f8c..000000000 --- a/src/client/actions/UserNoteUpdate.js +++ /dev/null @@ -1,30 +0,0 @@ -const Action = require('./Action'); -const { Events } = require('../../util/Constants'); - -class UserNoteUpdateAction extends Action { - handle(data) { - const client = this.client; - - const oldNote = client.user.notes.get(data.id); - const note = data.note.length ? data.note : null; - - client.user.notes.set(data.id, note); - - client.emit(Events.USER_NOTE_UPDATE, data.id, oldNote, note); - - return { - old: oldNote, - updated: note, - }; - } -} - -/** - * Emitted whenever a note is updated. - * @event Client#userNoteUpdate - * @param {User} user The user the note belongs to - * @param {string} oldNote The note content before the update - * @param {string} newNote The note content after the update - */ - -module.exports = UserNoteUpdateAction; diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js index 186f234fd..f9d7b13ae 100644 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ b/src/client/websocket/packets/WebSocketPacketManager.js @@ -37,9 +37,6 @@ class WebSocketPacketManager { this.register(WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate')); this.register(WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate')); this.register(WSEvents.USER_UPDATE, require('./handlers/UserUpdate')); - this.register(WSEvents.USER_NOTE_UPDATE, require('./handlers/UserNoteUpdate')); - this.register(WSEvents.USER_SETTINGS_UPDATE, require('./handlers/UserSettingsUpdate')); - this.register(WSEvents.USER_GUILD_SETTINGS_UPDATE, require('./handlers/UserGuildSettingsUpdate')); this.register(WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate')); this.register(WSEvents.TYPING_START, require('./handlers/TypingStart')); this.register(WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate')); @@ -47,9 +44,6 @@ class WebSocketPacketManager { this.register(WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate')); this.register(WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk')); this.register(WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate')); - this.register(WSEvents.GUILD_SYNC, require('./handlers/GuildSync')); - this.register(WSEvents.RELATIONSHIP_ADD, require('./handlers/RelationshipAdd')); - this.register(WSEvents.RELATIONSHIP_REMOVE, require('./handlers/RelationshipRemove')); this.register(WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd')); this.register(WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove')); this.register(WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll')); diff --git a/src/client/websocket/packets/handlers/GuildSync.js b/src/client/websocket/packets/handlers/GuildSync.js deleted file mode 100644 index 0b9f5aa86..000000000 --- a/src/client/websocket/packets/handlers/GuildSync.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildSyncHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildSync.handle(data); - } -} - -module.exports = GuildSyncHandler; diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index 367406ba0..0e48e5586 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -9,9 +9,6 @@ class ReadyHandler extends AbstractHandler { client.ws.heartbeat(); - 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; @@ -20,27 +17,8 @@ class ReadyHandler extends AbstractHandler { for (const guild of data.guilds) client.guilds.add(guild); for (const privateDM of data.private_channels) client.channels.add(privateDM); - - for (const relation of data.relationships) { - const user = client.users.add(relation.user); - if (relation.type === 1) { - client.user.friends.set(user.id, user); - } else if (relation.type === 2) { - client.user.blocked.set(user.id, user); - } - } - for (const presence of data.presences || []) client.presences.add(presence); - if (data.notes) { - for (const user in data.notes) { - let note = data.notes[user]; - if (!note.length) note = null; - - client.user.notes.set(user, note); - } - } - if (!client.users.has('1')) { client.users.add({ id: '1', @@ -61,7 +39,6 @@ class ReadyHandler extends AbstractHandler { client.setMaxListeners(data.guilds.length + 10); client.once('ready', () => { - client.syncGuilds(); client.setMaxListeners(10); client.clearTimeout(t); }); diff --git a/src/client/websocket/packets/handlers/RelationshipAdd.js b/src/client/websocket/packets/handlers/RelationshipAdd.js deleted file mode 100644 index f416ff1d6..000000000 --- a/src/client/websocket/packets/handlers/RelationshipAdd.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class RelationshipAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - if (data.type === 1) { - client.users.fetch(data.id).then(user => { - client.user.friends.set(user.id, user); - }); - } else if (data.type === 2) { - client.users.fetch(data.id).then(user => { - client.user.blocked.set(user.id, user); - }); - } - } -} - -module.exports = RelationshipAddHandler; diff --git a/src/client/websocket/packets/handlers/RelationshipRemove.js b/src/client/websocket/packets/handlers/RelationshipRemove.js deleted file mode 100644 index b57326ad6..000000000 --- a/src/client/websocket/packets/handlers/RelationshipRemove.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class RelationshipRemoveHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - if (data.type === 2) { - if (client.user.blocked.has(data.id)) { - client.user.blocked.delete(data.id); - } - } else if (data.type === 1) { - if (client.user.friends.has(data.id)) { - client.user.friends.delete(data.id); - } - } - } -} - -module.exports = RelationshipRemoveHandler; diff --git a/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js b/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js deleted file mode 100644 index 6d92e76bb..000000000 --- a/src/client/websocket/packets/handlers/UserGuildSettingsUpdate.js +++ /dev/null @@ -1,21 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); -const ClientUserGuildSettings = require('../../../../structures/ClientUserGuildSettings'); - -class UserGuildSettingsUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const settings = client.user.guildSettings.get(packet.d.guild_id); - if (settings) settings.patch(packet.d); - else client.user.guildSettings.set(packet.d.guild_id, new ClientUserGuildSettings(this.client, packet.d)); - client.emit(Events.USER_GUILD_SETTINGS_UPDATE, client.user.guildSettings.get(packet.d.guild_id)); - } -} - -/** - * Emitted whenever the client user's settings update. - * @event Client#clientUserGuildSettingsUpdate - * @param {ClientUserGuildSettings} clientUserGuildSettings The new client user guild settings - */ - -module.exports = UserGuildSettingsUpdateHandler; diff --git a/src/client/websocket/packets/handlers/UserNoteUpdate.js b/src/client/websocket/packets/handlers/UserNoteUpdate.js deleted file mode 100644 index 1e4777a39..000000000 --- a/src/client/websocket/packets/handlers/UserNoteUpdate.js +++ /dev/null @@ -1,12 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class UserNoteUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - - client.actions.UserNoteUpdate.handle(data); - } -} - -module.exports = UserNoteUpdateHandler; diff --git a/src/client/websocket/packets/handlers/UserSettingsUpdate.js b/src/client/websocket/packets/handlers/UserSettingsUpdate.js deleted file mode 100644 index ad322dca1..000000000 --- a/src/client/websocket/packets/handlers/UserSettingsUpdate.js +++ /dev/null @@ -1,18 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class UserSettingsUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - client.user.settings.patch(packet.d); - client.emit(Events.USER_SETTINGS_UPDATE, client.user.settings); - } -} - -/** - * Emitted whenever the client user's settings update. - * @event Client#clientUserSettingsUpdate - * @param {ClientUserSettings} clientUserSettings The new client user settings - */ - -module.exports = UserSettingsUpdateHandler; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 6905cbbc5..97f6af280 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -6,8 +6,6 @@ const Messages = { TOKEN_INVALID: 'An invalid token was provided.', TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', - FEATURE_USER_ONLY: 'Only user accounts are able to make use of this feature.', - WS_CONNECTION_TIMEOUT: 'The connection to the gateway timed out.', WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, diff --git a/src/index.js b/src/index.js index b89d9ce56..aad9c9433 100644 --- a/src/index.js +++ b/src/index.js @@ -56,9 +56,6 @@ module.exports = { // 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'), Collector: require('./structures/interfaces/Collector'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index cf6227a8a..1d2f66e5e 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -25,7 +25,7 @@ class RESTManager { getAuth() { const token = this.client.token || this.client.accessToken; - const prefixed = !!this.client.application || (this.client.user && this.client.user.bot); + const prefixed = !!this.client.application || this.client.user; if (token && prefixed) return `${this.tokenPrefix} ${token}`; else if (token) return token; throw new Error('TOKEN_MISSING'); diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index c42bf5e49..0cf9a1157 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -39,7 +39,7 @@ class GuildStore extends DataStore { /** * Creates a guild. - * This is only available to bots in less than 10 guilds and user accounts. + * This is only available to bots in fewer than 10 guilds. * @param {string} name The name of the guild * @param {Object} [options] Options for the creating * @param {string} [options.region] The region for the server, defaults to the closest one available diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 07b85ea87..2d7049792 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -1,7 +1,6 @@ const DataStore = require('./DataStore'); const Collection = require('../util/Collection'); const Message = require('../structures/Message'); -const { Error } = require('../errors'); /** * Stores messages for text-based channels. @@ -80,12 +79,6 @@ class MessageStore extends DataStore { } async _fetchId(messageID) { - if (!this.client.user.bot) { - const messages = await this._fetchMany({ limit: 1, around: messageID }); - const msg = messages.get(messageID); - if (!msg) throw new Error('MESSAGE_MISSING'); - return msg; - } const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); return this.add(data); } diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 48e988405..d700877c5 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -45,7 +45,6 @@ class UserStore extends DataStore { /** * Obtains a user from Discord, or the user cache if it's already available. - * This is only available when using a bot account. * @param {Snowflake} id ID of the user * @param {boolean} [cache=true] Whether to cache the new user object if it isn't already * @returns {Promise} diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index b884dfd11..59dc82671 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -170,26 +170,6 @@ class ClientApplication extends Base { } }); } - /** - * Resets the app's secret. - * This is only available when using a user account. - * @returns {Promise} - */ - resetSecret() { - return this.client.api.oauth2.applications[this.id].reset.post() - .then(app => new ClientApplication(this.client, app)); - } - - /** - * Resets the app's bot token. - * This is only available when using a user account. - * @returns {Promise} - */ - resetToken() { - return this.client.api.oauth2.applications[this.id].bot.reset.post() - .then(app => new ClientApplication(this.client, Object.assign({}, this, { bot: app }))); - } - /** * When concatenated with a string, this automatically returns the application's name instead of the * ClientApplication object. diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index cce9957ad..31d966e6d 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,10 +1,5 @@ const Structures = require('../util/Structures'); -const Collection = require('../util/Collection'); -const ClientUserSettings = require('./ClientUserSettings'); -const ClientUserGuildSettings = require('./ClientUserGuildSettings'); -const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); -const Guild = require('./Guild'); /** * Represents the logged in client's Discord user. @@ -20,75 +15,14 @@ class ClientUser extends Structures.get('User') { */ this.verified = data.verified; - /** - * The email of this account - * This is only filled when using a user account. - * @type {?string} - */ - this.email = data.email; this._typing = new Map(); /** - * A Collection of friends for the logged in user - * This is only filled when using a user account. - * @type {Collection} - */ - this.friends = new Collection(); - - /** - * A Collection of blocked users for the logged in user - * This is only filled when using a user account. - * @type {Collection} - */ - this.blocked = new Collection(); - - /** - * A Collection of notes for the logged in user - * This is only filled when using a user account. - * @type {Collection} - */ - this.notes = new Collection(); - - /** - * If the user has Discord premium (nitro) - * This is only filled when using a user account. - * @type {?boolean} - */ - this.premium = typeof data.premium === 'boolean' ? data.premium : null; - - /** - * If the user has MFA enabled on their account - * This is only filled when using a user account. + * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account * @type {?boolean} */ this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; - /** - * If the user has ever used a mobile device on Discord - * This is only filled when using a user account. - * @type {?boolean} - */ - this.mobile = typeof data.mobile === 'boolean' ? data.mobile : null; - - /** - * Various settings for this user - * This is only filled when using a user account. - * @type {?ClientUserSettings} - */ - this.settings = data.user_settings ? new ClientUserSettings(this, data.user_settings) : null; - - /** - * All of the user's guild settings - * This is only filled when using a user account. - * @type {Collection} - */ - this.guildSettings = new Collection(); - if (data.user_guild_settings) { - for (const settings of data.user_guild_settings) { - this.guildSettings.set(settings.guild_id, new ClientUserGuildSettings(this.client, settings)); - } - } - if (data.token) this.client.token = data.token; } @@ -101,15 +35,7 @@ class ClientUser extends Structures.get('User') { return this.client.presences.clientPresence; } - edit(data, passcode) { - if (!this.bot) { - if (typeof passcode !== 'object') { - data.password = passcode; - } else { - data.code = passcode.mfaCode; - data.password = passcode.password; - } - } + edit(data) { return this.client.api.users('@me').patch({ data }) .then(newData => { this.client.token = newData.token; @@ -122,7 +48,6 @@ class ClientUser extends Structures.get('User') { * Changing usernames in Discord is heavily rate limited, with only 2 requests * every hour. Use this sparingly! * @param {string} username The new username - * @param {string} [password] Current password (only for user accounts) * @returns {Promise} * @example * // Set username @@ -130,43 +55,8 @@ class ClientUser extends Structures.get('User') { * .then(user => console.log(`My new username is ${user.username}`)) * .catch(console.error); */ - setUsername(username, password) { - return this.edit({ username }, password); - } - - /** - * Changes the email for the client user's account. - * This is only available when using a user account. - * @param {string} email New email to change to - * @param {string} password Current password - * @returns {Promise} - * @example - * // Set email - * client.user.setEmail('bob@gmail.com', 'some amazing password 123') - * .then(user => console.log(`My new email is ${user.email}`)) - * .catch(console.error); - */ - setEmail(email, password) { - return this.edit({ email }, password); - } - - /** - * Changes the password for the client user's account. - * This is only available when using a user account. - * @param {string} newPassword New password to change to - * @param {Object|string} options Object containing an MFA code, password or both. - * Can be just a string for the password. - * @param {string} [options.oldPassword] Current password - * @param {string} [options.mfaCode] Timed MFA Code - * @returns {Promise} - * @example - * // Set password - * client.user.setPassword('some new amazing password 456', 'some amazing password 123') - * .then(user => console.log('New password set!')) - * .catch(console.error); - */ - setPassword(newPassword, options) { - return this.edit({ new_password: newPassword }, { password: options.oldPassword, mfaCode: options.mfaCode }); + setUsername(username) { + return this.edit({ username }); } /** @@ -262,36 +152,6 @@ class ClientUser extends Structures.get('User') { return this.setPresence({ afk }); } - /** - * 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 {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 - * @param {GuildResolvable} [options.guild] Limit the search to a specific guild - * @returns {Promise} - * @example - * // Fetch mentions - * client.user.fetchMentions() - * .then(console.log) - * .catch(console.error); - * @example - * // Fetch mentions from a guild - * client.user.fetchMentions({ - * guild: '222078108977594368' - * }) - * .then(console.log) - * .catch(console.error); - */ - fetchMentions(options = {}) { - if (options.guild instanceof Guild) options.guild = options.guild.id; - Util.mergeDefault({ limit: 25, roles: true, everyone: true, guild: null }, options); - - return this.client.api.users('@me').mentions.get({ query: options }) - .then(data => data.map(m => this.client.channels.get(m.channel_id).messages.add(m, false))); - } - /** * An object containing either a user or access token, and an optional nickname. * @typedef {Object} GroupDMRecipientOptions @@ -327,16 +187,6 @@ class ClientUser extends Structures.get('User') { return this.client.api.users('@me').channels.post({ data }) .then(res => this.client.channels.add(res)); } - - toJSON() { - return super.toJSON({ - friends: false, - blocked: false, - notes: false, - settings: false, - guildSettings: false, - }); - } } module.exports = ClientUser; diff --git a/src/structures/ClientUserChannelOverride.js b/src/structures/ClientUserChannelOverride.js deleted file mode 100644 index 680b95518..000000000 --- a/src/structures/ClientUserChannelOverride.js +++ /dev/null @@ -1,28 +0,0 @@ -const { UserChannelOverrideMap } = require('../util/Constants'); - -/** - * A wrapper around the ClientUser's channel overrides. - */ -class ClientUserChannelOverride { - constructor(data) { - this.patch(data); - } - - /** - * Patch the data contained in this class with new partial data. - * @param {Object} data Data to patch this with - * @private - */ - patch(data) { - for (const [key, value] of Object.entries(UserChannelOverrideMap)) { - if (!data.hasOwnProperty(key)) continue; - if (typeof value === 'function') { - this[value.name] = value(data[key]); - } else { - this[value] = data[key]; - } - } - } -} - -module.exports = ClientUserChannelOverride; diff --git a/src/structures/ClientUserGuildSettings.js b/src/structures/ClientUserGuildSettings.js deleted file mode 100644 index ef1ba9a57..000000000 --- a/src/structures/ClientUserGuildSettings.js +++ /dev/null @@ -1,60 +0,0 @@ -const { UserGuildSettingsMap } = require('../util/Constants'); -const Collection = require('../util/Collection'); -const ClientUserChannelOverride = require('./ClientUserChannelOverride'); - -/** - * A wrapper around the ClientUser's guild settings. - */ -class ClientUserGuildSettings { - constructor(client, data) { - /** - * The client that created the instance of the ClientUserGuildSettings - * @name ClientUserGuildSettings#client - * @type {Client} - * @readonly - */ - Object.defineProperty(this, 'client', { value: client }); - /** - * The ID of the guild these settings are for - * @type {Snowflake} - */ - this.guildID = data.guild_id; - this.channelOverrides = new Collection(); - this.patch(data); - } - - /** - * Patch the data contained in this class with new partial data. - * @param {Object} data Data to patch this with - * @private - */ - patch(data) { - for (const [key, value] of Object.entries(UserGuildSettingsMap)) { - if (!data.hasOwnProperty(key)) continue; - if (key === 'channel_overrides') { - for (const channel of data[key]) { - const override = this.channelOverrides.get(channel.channel_id); - if (override) override.patch(channel); - else this.channelOverrides.set(channel.channel_id, new ClientUserChannelOverride(channel)); - } - } else if (typeof value === 'function') { - this[value.name] = value(data[key]); - } else { - this[value] = data[key]; - } - } - } - - /** - * Update a specific property of the guild settings. - * @param {string} name Name of property - * @param {*} value Value to patch - * @returns {Promise} - * @private - */ - update(name, value) { - return this.client.api.users('@me').guilds(this.guildID).settings.patch({ data: { [name]: value } }); - } -} - -module.exports = ClientUserGuildSettings; diff --git a/src/structures/ClientUserSettings.js b/src/structures/ClientUserSettings.js deleted file mode 100644 index 20bb82d1a..000000000 --- a/src/structures/ClientUserSettings.js +++ /dev/null @@ -1,80 +0,0 @@ -const { UserSettingsMap } = require('../util/Constants'); -const Util = require('../util/Util'); -const { Error } = require('../errors'); - -/** - * A wrapper around the ClientUser's settings. - */ -class ClientUserSettings { - constructor(user, data) { - this.user = user; - this.patch(data); - } - - /** - * Patch the data contained in this class with new partial data. - * @param {Object} data Data to patch this with - * @private - */ - patch(data) { - for (const [key, value] of Object.entries(UserSettingsMap)) { - if (!data.hasOwnProperty(key)) continue; - if (typeof value === 'function') { - this[value.name] = value(data[key]); - } else { - this[value] = data[key]; - } - } - } - - /** - * Update a specific property of of user settings. - * @param {string} name Name of property - * @param {*} value Value to patch - * @returns {Promise} - * @private - */ - update(name, value) { - return this.user.client.api.users['@me'].settings.patch({ data: { [name]: value } }); - } - - /** - * Sets the position of the guild in the guild listing. - * @param {Guild} guild The guild to move - * @param {number} position Absolute or relative position - * @param {boolean} [relative=false] Whether to position relatively or absolutely - * @returns {Promise} - */ - setGuildPosition(guild, position, relative) { - const temp = Object.assign([], this.guildPositions); - Util.moveElementInArray(temp, guild.id, position, relative); - return this.update('guild_positions', temp).then(() => guild); - } - - /** - * Adds a guild to the list of restricted guilds. - * @param {Guild} guild The guild to add - * @returns {Promise} - */ - addRestrictedGuild(guild) { - const temp = Object.assign([], this.restrictedGuilds); - if (temp.includes(guild.id)) return Promise.reject(new Error('GUILD_RESTRICTED', true)); - temp.push(guild.id); - return this.update('restricted_guilds', temp).then(() => guild); - } - - /** - * Removes a guild from the list of restricted guilds. - * @param {Guild} guild The guild to remove - * @returns {Promise} - */ - removeRestrictedGuild(guild) { - const temp = Object.assign([], this.restrictedGuilds); - const index = temp.indexOf(guild.id); - if (index < 0) return Promise.reject(new Error('GUILD_RESTRICTED')); - temp.splice(index, 1); - return this.update('restricted_guilds', temp).then(() => guild); - } -} - -module.exports = ClientUserSettings; diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index d67d33ad2..933c701bb 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -184,16 +184,12 @@ class GroupDMChannel extends Channel { * @param {Object} options Options for this method * @param {UserResolvable} options.user User to add to this Group DM * @param {string} [options.accessToken] Access token to use to add the user to this Group DM - * (only available under a bot account) - * @param {string} [options.nick] Permanent nickname to give the user (only available under a bot account) + * @param {string} [options.nick] Permanent nickname to give the user * @returns {Promise} */ addUser({ user, accessToken, nick }) { const id = this.client.users.resolveID(user); - const data = this.client.user.bot ? - { nick, access_token: accessToken } : - { recipient: id }; - return this.client.api.channels[this.id].recipients[id].put({ data }) + return this.client.api.channels[this.id].recipients[id].put({ nick, access_token: accessToken }) .then(() => this); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 812250630..184513159 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -7,7 +7,6 @@ const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Snowflake = require('../util/Snowflake'); -const Shared = require('./shared'); const GuildMemberStore = require('../stores/GuildMemberStore'); const RoleStore = require('../stores/RoleStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); @@ -352,79 +351,6 @@ class Guild extends Base { return this.client.voice.connections.get(this.id) || null; } - /** - * The position of this guild - * This is only available when using a user account. - * @type {?number} - * @readonly - */ - get position() { - if (this.client.user.bot) return null; - if (!this.client.user.settings.guildPositions) return null; - return this.client.user.settings.guildPositions.indexOf(this.id); - } - - /** - * Whether the guild is muted - * This is only available when using a user account. - * @type {?boolean} - * @readonly - */ - get muted() { - if (this.client.user.bot) return null; - try { - return this.client.user.guildSettings.get(this.id).muted; - } catch (err) { - return false; - } - } - - /** - * The type of message that should notify you - * one of `EVERYTHING`, `MENTIONS`, `NOTHING` - * This is only available when using a user account. - * @type {?string} - * @readonly - */ - get messageNotifications() { - if (this.client.user.bot) return null; - try { - return this.client.user.guildSettings.get(this.id).messageNotifications; - } catch (err) { - return null; - } - } - - /** - * Whether to receive mobile push notifications - * This is only available when using a user account. - * @type {?boolean} - * @readonly - */ - get mobilePush() { - if (this.client.user.bot) return null; - try { - return this.client.user.guildSettings.get(this.id).mobilePush; - } catch (err) { - return false; - } - } - - /** - * Whether to suppress everyone messages - * This is only available when using a user account. - * @type {?boolean} - * @readonly - */ - get suppressEveryone() { - if (this.client.user.bot) return null; - try { - return this.client.user.guildSettings.get(this.id).suppressEveryone; - } catch (err) { - return null; - } - } - /** * The `@everyone` role of the guild * @type {?Role} @@ -597,26 +523,6 @@ class Guild extends Base { .then(data => this.members.add(data)); } - /** - * Performs a search within the entire guild. - * This is only available when using a user account. - * @param {MessageSearchOptions} [options={}] Options to pass to the search - * @returns {Promise} - * @example - * guild.search({ - * content: 'discord.js', - * before: '2016-11-17' - * }) - * .then(res => { - * const hit = res.results[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**, total results: ${res.total}`); - * }) - * .catch(console.error); - */ - search(options = {}) { - return Shared.search(this, options); - } - /** * The data for editing a guild. * @typedef {Object} GuildEditData @@ -831,55 +737,6 @@ class Guild extends Base { return this.edit({ splash: await DataResolver.resolveImage(splash), reason }); } - /** - * Sets the position of the guild in the guild listing. - * This is only available when using a user account. - * @param {number} position Absolute or relative position - * @param {boolean} [relative=false] Whether to position relatively or absolutely - * @returns {Promise} - */ - setPosition(position, relative) { - if (this.client.user.bot) { - return Promise.reject(new Error('FEATURE_USER_ONLY')); - } - return this.client.user.settings.setGuildPosition(this, position, relative); - } - - /** - * Marks all messages in this guild as read. - * This is only available when using a user account. - * @returns {Promise} - */ - acknowledge() { - return this.client.api.guilds(this.id).ack - .post({ data: { token: this.client.rest._ackToken } }) - .then(res => { - if (res.token) this.client.rest._ackToken = res.token; - return this; - }); - } - - /** - * Whether to allow direct messages from guild members. - * This is only available when using a user account. - * @param {boolean} allow Whether to allow direct messages - * @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); - } - - /** - * Syncs this guild (already done automatically every 30 seconds). - * This is only available when using a user account. - */ - sync() { - if (!this.client.user.bot) this.client.syncGuilds([this]); - } - /** * The data needed for updating a channel's position. * @typedef {Object} ChannelPosition diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 97a28681b..36a84f529 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -6,7 +6,6 @@ const PermissionOverwrites = require('./PermissionOverwrites'); const Util = require('../util/Util'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); -const { MessageNotificationTypes } = require('../util/Constants'); const { Error, TypeError } = require('../errors'); /** @@ -564,37 +563,6 @@ class GuildChannel extends Channel { delete(reason) { return this.client.api.channels(this.id).delete({ reason }).then(() => this); } - - /** - * Whether the channel is muted - * This is only available when using a user account. - * @type {?boolean} - * @readonly - */ - get muted() { - if (this.client.user.bot) return null; - try { - return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).muted; - } catch (err) { - return false; - } - } - - /** - * The type of message that should notify you - * one of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` - * This is only available when using a user account. - * @type {?string} - * @readonly - */ - get messageNotifications() { - if (this.client.user.bot) return null; - try { - return this.client.user.guildSettings.get(this.guild.id).channelOverrides.get(this.id).messageNotifications; - } catch (err) { - return MessageNotificationTypes[3]; - } - } } module.exports = GuildChannel; diff --git a/src/structures/Message.js b/src/structures/Message.js index 3774d632d..aea85a604 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -499,20 +499,6 @@ class Message extends Base { return this.channel.send(content, Object.assign(options, { reply: this.member || this.author })); } - /** - * Marks the message as read. - * This is only available when using a user account. - * @returns {Promise} - */ - acknowledge() { - return this.client.api.channels(this.channel.id).messages(this.id).ack - .post({ data: { token: this.client.rest._ackToken } }) - .then(res => { - if (res.token) this.client.rest._ackToken = res.token; - return this; - }); - } - /** * Fetches the webhook used to create this message. * @returns {Promise} diff --git a/src/structures/User.js b/src/structures/User.js index e020d288a..b05ddc241 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -1,6 +1,5 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { Presence } = require('./Presence'); -const UserProfile = require('./UserProfile'); const Snowflake = require('../util/Snowflake'); const Base = require('./Base'); const { Error } = require('../errors'); @@ -151,16 +150,6 @@ class User extends Base { return `${this.username}#${this.discriminator}`; } - /** - * The note that is set for the user - * This is only available when using a user account. - * @type {?string} - * @readonly - */ - get note() { - return this.client.user.notes.get(this.id) || null; - } - /** * Checks whether the user is typing in a channel. * @param {ChannelResolvable} channel The channel to check in @@ -222,26 +211,6 @@ class User extends Base { .then(data => this.client.actions.ChannelDelete.handle(data).channel); } - /** - * Gets the profile of the user. - * This is only available when using a user account. - * @returns {Promise} - */ - fetchProfile() { - return this.client.api.users(this.id).profile.get().then(data => new UserProfile(this, data)); - } - - /** - * Sets a note for the user. - * This is only available when using a user account. - * @param {string} note The note to set for the user - * @returns {Promise} - */ - setNote(note) { - return this.client.api.users('@me').notes(this.id).put({ data: { note } }) - .then(() => this); - } - /** * Checks if the user is equal to another. It compares ID, username, discriminator, avatar, and bot flags. * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. diff --git a/src/structures/UserProfile.js b/src/structures/UserProfile.js deleted file mode 100644 index 704cb0748..000000000 --- a/src/structures/UserProfile.js +++ /dev/null @@ -1,83 +0,0 @@ -const Collection = require('../util/Collection'); -const { UserFlags } = require('../util/Constants'); -const UserConnection = require('./UserConnection'); -const Base = require('./Base'); - -/** - * Represents a user's profile on Discord. - * @extends {Base} - */ -class UserProfile extends Base { - constructor(user, data) { - super(user.client); - - /** - * The owner of the profile - * @type {User} - */ - this.user = user; - - /** - * The guilds that the client user and the user share - * @type {Collection} - */ - this.mutualGuilds = new Collection(); - - /** - * The user's connections - * @type {Collection} - */ - this.connections = new Collection(); - - this._patch(data); - } - - _patch(data) { - /** - * If the user has Discord Premium - * @type {boolean} - */ - this.premium = Boolean(data.premium_since); - - /** - * The Bitfield of the users' flags - * @type {number} - * @private - */ - this._flags = data.user.flags; - - /** - * The date since which the user has had Discord Premium - * @type {?Date} - */ - this.premiumSince = data.premium_since ? new Date(data.premium_since) : null; - - for (const guild of data.mutual_guilds) { - if (this.client.guilds.has(guild.id)) { - this.mutualGuilds.set(guild.id, this.client.guilds.get(guild.id)); - } - } - for (const connection of data.connected_accounts) { - this.connections.set(connection.id, new UserConnection(this.user, connection)); - } - } - - /** - * The flags the user has - * @type {UserFlags[]} - * @readonly - */ - get flags() { - const flags = []; - for (const [name, flag] of Object.entries(UserFlags)) { - if ((this._flags & flag) === flag) flags.push(name); - } - return flags; - } - - toJSON() { - return super.toJSON({ flags: true }); - } -} - -module.exports = UserProfile; diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 9659a61c0..b7b487dfa 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -119,23 +119,6 @@ class TextBasedChannel { return Shared.sendMessage(this, options); } - /** - * Performs a search within the channel. - * This is only available when using a user account. - * @param {MessageSearchOptions} [options={}] Options to pass to the search - * @returns {Promise} - * @example - * channel.search({ content: 'discord.js', before: '2016-11-17' }) - * .then(res => { - * const hit = res.results[0].find(m => m.hit).content; - * console.log(`I found: **${hit}**, total results: ${res.total}`); - * }) - * .catch(console.error); - */ - search(options = {}) { - return Shared.search(this, options); - } - /** * Starts a typing indicator in the channel. * @param {number} [count=1] The number of times startTyping should be considered to have been called @@ -273,7 +256,6 @@ class TextBasedChannel { /** * Bulk deletes given messages that are newer than two weeks. - * This is only available when using a bot account. * @param {Collection|Message[]|Snowflake[]|number} messages * Messages or number of messages to delete * @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically @@ -316,28 +298,11 @@ class TextBasedChannel { throw new TypeError('MESSAGE_BULK_DELETE_TYPE'); } - /** - * Marks all messages in this channel as read. - * This is only available when using a user account. - * @returns {Promise} - */ - acknowledge() { - if (!this.lastMessageID) return Promise.resolve(this); - return this.client.api.channels[this.id].messages[this.lastMessageID].ack - .post({ data: { token: this.client.rest._ackToken } }) - .then(res => { - if (res.token) this.client.rest._ackToken = res.token; - return this; - }); - } - static applyToClass(structure, full = false, ignore = []) { const props = ['send']; if (full) { props.push( - 'acknowledge', 'lastMessage', - 'search', 'bulkDelete', 'startTyping', 'stopTyping', diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js deleted file mode 100644 index db30b572c..000000000 --- a/src/structures/shared/Search.js +++ /dev/null @@ -1,100 +0,0 @@ -const Util = require('../../util/Util'); -const { TypeError } = require('../../errors'); - -/** - * @typedef {Object} MessageSearchOptions - * @property {string} [content] Message content - * @property {Snowflake} [maxID] Maximum ID for the filter - * @property {Snowflake} [minID] Minimum ID for the filter - * @property {string} [has] One of `link`, `embed`, `file`, `video`, `image`, or `sound`, - * or add `-` to negate (e.g. `-file`) - * @property {ChannelResolvable} [channel] Channel to limit search to (only for guild search endpoint) - * @property {UserResolvable} [author] Author to limit search - * @property {string} [authorType] One of `user`, `bot`, `webhook`, or add `-` to negate (e.g. `-webhook`) - * @property {string} [sortBy='timestamp'] `timestamp` or `relevant` - * @property {string} [sortOrder='descending'] `ascending` or `descending` - * @property {number} [contextSize=2] How many messages to get around the matched message (0 to 2) - * @property {number} [limit=25] Maximum number of results to get (1 to 25) - * @property {number} [offset=0] Offset the "pages" of results (since you can only see 25 at a time) - * @property {UserResolvable} [mentions] Mentioned user filter - * @property {boolean} [mentionsEveryone] If everyone is mentioned - * @property {string} [linkHostname] Filter links by hostname - * @property {string} [embedProvider] The name of an embed provider - * @property {string} [embedType] one of `image`, `video`, `url`, `rich`, or add `-` to negate (e.g. `-image`) - * @property {string} [attachmentFilename] The name of an attachment - * @property {string} [attachmentExtension] The extension of an attachment - * @property {Date} [before] Date to find messages before - * @property {Date} [after] Date to find messages before - * @property {Date} [during] Date to find messages during (range of date to date + 24 hours) - * @property {boolean} [nsfw=false] Include results from NSFW channels - */ - -/** - * @typedef {Object} MessageSearchResult - * @property {number} total Total result count - * @property {Array} results Array of message results - * The message which has triggered the result will have the `hit` property set to `true` - */ - -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 = 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 = 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 = 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); - if (options.mentions) options.mentions = target.client.users.resolveID(options.options.mentions); - if (options.sortOrder) { - options.sortOrder = { ascending: 'asc', descending: 'desc' }[options.sortOrder] || options.sortOrder; - } - options = { - content: options.content, - max_id: options.maxID, - min_id: options.minID, - has: options.has, - channel_id: options.channel, - author_id: options.author, - author_type: options.authorType, - context_size: options.contextSize, - sort_by: options.sortBy, - sort_order: options.sortOrder, - limit: options.limit, - offset: options.offset, - mentions: options.mentions, - mentions_everyone: options.mentionsEveryone, - link_hostname: options.linkHostname, - embed_provider: options.embedProvider, - embed_type: options.embedType, - attachment_filename: options.attachmentFilename, - attachment_extension: options.attachmentExtension, - include_nsfw: options.nsfw, - }; - - // Lazy load these because some of them use util - const Channel = require('../Channel'); - const Guild = require('../Guild'); - - if (!(target instanceof Channel || target instanceof Guild)) throw new TypeError('SEARCH_CHANNEL_TYPE'); - - let endpoint = target.client.api[target instanceof Channel ? 'channels' : 'guilds'](target.id).messages().search; - return endpoint.get({ query: options }).then(body => { - const results = body.messages.map(x => - x.map(m => target.client.channels.get(m.channel_id).messages.add(m, false)) - ); - return { - total: body.total_results, - results, - }; - }); -}; diff --git a/src/structures/shared/index.js b/src/structures/shared/index.js index 67a09646b..5e81ec3c0 100644 --- a/src/structures/shared/index.js +++ b/src/structures/shared/index.js @@ -1,5 +1,4 @@ module.exports = { - search: require('./Search'), sendMessage: require('./SendMessage'), createMessage: require('./CreateMessage'), }; diff --git a/src/util/Constants.js b/src/util/Constants.js index 4c381e4d6..9b7702e25 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,7 +21,6 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) * @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone} - * @property {boolean} [sync=false] Whether to periodically sync guilds (for user accounts) * @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST @@ -46,7 +45,6 @@ exports.DefaultOptions = { messageSweepInterval: 0, fetchAllMembers: false, disableEveryone: false, - sync: false, restWsBridgeTimeout: 5000, disabledEvents: [], restTimeOffset: 500, @@ -246,7 +244,6 @@ exports.Events = { USER_UPDATE: 'userUpdate', USER_NOTE_UPDATE: 'userNoteUpdate', USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', - USER_GUILD_SETTINGS_UPDATE: 'clientUserGuildSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', VOICE_BROADCAST_SUBSCRIBE: 'subscribe', @@ -264,7 +261,6 @@ exports.Events = { * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: * * READY * * RESUMED - * * GUILD_SYNC * * GUILD_CREATE * * GUILD_DELETE * * GUILD_UPDATE @@ -302,7 +298,6 @@ exports.Events = { exports.WSEvents = keyMirror([ 'READY', 'RESUMED', - 'GUILD_SYNC', 'GUILD_CREATE', 'GUILD_DELETE', 'GUILD_UPDATE', @@ -328,15 +323,10 @@ exports.WSEvents = keyMirror([ 'MESSAGE_REACTION_REMOVE', 'MESSAGE_REACTION_REMOVE_ALL', 'USER_UPDATE', - 'USER_NOTE_UPDATE', - 'USER_SETTINGS_UPDATE', - 'USER_GUILD_SETTINGS_UPDATE', 'PRESENCE_UPDATE', 'VOICE_STATE_UPDATE', 'TYPING_START', 'VOICE_SERVER_UPDATE', - 'RELATIONSHIP_ADD', - 'RELATIONSHIP_REMOVE', ]); /** @@ -392,13 +382,6 @@ exports.ExplicitContentFilterTypes = [ 'FRIENDS_AND_NON_FRIENDS', ]; -exports.MessageNotificationTypes = [ - 'EVERYTHING', - 'MENTIONS', - 'NOTHING', - 'INHERIT', -]; - exports.UserSettingsMap = { /** * Automatically convert emoticons in your messages to emoji, @@ -532,60 +515,6 @@ exports.UserSettingsMap = { }, }; -exports.UserGuildSettingsMap = { - message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching - /** - * The type of message that should notify you. - * One of `EVERYTHING`, `MENTIONS`, `NOTHING` - * @name ClientUserGuildSettings#messageNotifications - * @type {string} - */ - return exports.MessageNotificationTypes[type]; - }, - /** - * Whether to receive mobile push notifications - * @name ClientUserGuildSettings#mobilePush - * @type {boolean} - */ - mobile_push: 'mobilePush', - /** - * Whether the guild is muted or not - * @name ClientUserGuildSettings#muted - * @type {boolean} - */ - muted: 'muted', - /** - * Whether to suppress everyone messages - * @name ClientUserGuildSettings#suppressEveryone - * @type {boolean} - */ - suppress_everyone: 'suppressEveryone', - /** - * A collection containing all the channel overrides - * @name ClientUserGuildSettings#channelOverrides - * @type {Collection} - */ - channel_overrides: 'channelOverrides', -}; - -exports.UserChannelOverrideMap = { - message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching - /** - * The type of message that should notify you. - * One of `EVERYTHING`, `MENTIONS`, `NOTHING`, `INHERIT` - * @name ClientUserChannelOverride#messageNotifications - * @type {string} - */ - return exports.MessageNotificationTypes[type]; - }, - /** - * Whether the channel is muted or not - * @name ClientUserChannelOverride#muted - * @type {boolean} - */ - muted: 'muted', -}; - /** * All flags users can have: * * STAFF From ba32eec7e8903d6cd6d589c9fa3feda506a33537 Mon Sep 17 00:00:00 2001 From: Isabella Date: Wed, 25 Jul 2018 22:38:11 -0400 Subject: [PATCH 0756/1359] feat(Message): add url getter (#2594) * feat(Message): add jumpToURL getter * fix url * url syntax update * name to url --- src/structures/Message.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/structures/Message.js b/src/structures/Message.js index aea85a604..1b5d677d9 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -237,6 +237,15 @@ class Message extends Base { return this.channel.guild || null; } + /** + * The url to jump to this message + * @type {string} + * @readonly + */ + get url() { + return `https://discordapp.com/channels/${this.guild ? this.guild.id : '@me'}/${this.channel.id}/${this.id}`; + } + /** * The message contents with all mentions replaced by the equivalent text. * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted. From 92d753abe4b8f9e7b72eba9661692f7cb0bd1220 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Thu, 26 Jul 2018 04:38:56 +0200 Subject: [PATCH 0757/1359] build(deps): update ws requirement to ^6.0.0 (#2670) Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/commits/6.0.0) Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe26eb93d..11bfcbedf 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "pako": "^1.0.0", "prism-media": "hydrabolt/prism-media", "tweetnacl": "^1.0.0", - "ws": "^4.0.0" + "ws": "^6.0.0" }, "peerDependencies": { "bufferutil": "^3.0.0", From 9796489cbfa68a230bec46be8b05b3cb143953dc Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Thu, 26 Jul 2018 17:14:52 -0700 Subject: [PATCH 0758/1359] remove hard-coded domain in welcome logo --- 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 837e3a3a3..730134184 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -1,7 +1,7 @@

- +


From 88616eaf3e1854d1ce2f787658059f1617e8f34f Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Tue, 31 Jul 2018 09:55:32 -0600 Subject: [PATCH 0759/1359] fix: Shards not receiving regular messages --- src/sharding/ShardClientUtil.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 07c282ebf..1ca30bb34 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -132,6 +132,8 @@ class ShardClientUtil { } catch (err) { this._respond('eval', { _eval: message._eval, _error: Util.makePlainError(err) }); } + } else { + this.send(message); } } From 4ae58f66f4b46e789b3eb050f1dca9e09db71e0a Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Tue, 31 Jul 2018 19:43:17 +0300 Subject: [PATCH 0760/1359] fix: Wrong _patch call from GuildEmoji#edit and other issues (#2673) * Fix bugs * Make the data.roles be undefined before API call * Suggested changes * Handle edit properly --- src/structures/GuildEmoji.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 4811f207e..e9fde762a 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -23,21 +23,21 @@ class GuildEmoji extends Emoji { } _patch(data) { - this.name = data.name; + if (data.name) this.name = data.name; /** * Whether or not this emoji requires colons surrounding it * @type {boolean} */ - this.requiresColons = data.require_colons; + if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons; /** * Whether this emoji is managed by an external service * @type {boolean} */ - this.managed = data.managed; + if (typeof data.managed !== 'undefined') this.managed = data.managed; - if (data.roles) this.roles._patch(data.roles); + if (data.roles) this._roles = data.roles; } _clone() { @@ -114,14 +114,15 @@ class GuildEmoji extends Emoji { * .catch(console.error); */ edit(data, reason) { + const roles = data.roles ? data.roles.map(r => r.id || r) : undefined; return this.client.api.guilds(this.guild.id).emojis(this.id) .patch({ data: { name: data.name, - roles: data.roles ? data.roles.map(r => r.id ? r.id : r) : undefined, + roles, }, reason }) - .then(() => { + .then(newData => { const clone = this._clone(); - clone._patch(data); + clone._patch(newData); return clone; }); } From b38f537da89e02d7cce0af508cd07d284e2fa8ec Mon Sep 17 00:00:00 2001 From: Yukine Date: Sat, 4 Aug 2018 02:19:19 +0200 Subject: [PATCH 0761/1359] feat: Add new Permission PRIORITY_SPEAKER (#2698) --- src/util/Permissions.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 0a9c4a169..2f56b816f 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -148,6 +148,7 @@ class Permissions { * * `MANAGE_GUILD` (edit the guild information, region, etc.) * * `ADD_REACTIONS` (add new reactions to messages) * * `VIEW_AUDIT_LOG` + * * `PRIORITY_SPEAKER` * * `VIEW_CHANNEL` * * `SEND_MESSAGES` * * `SEND_TTS_MESSAGES` @@ -180,6 +181,7 @@ Permissions.FLAGS = { MANAGE_GUILD: 1 << 5, ADD_REACTIONS: 1 << 6, VIEW_AUDIT_LOG: 1 << 7, + PRIORITY_SPEAKER: 1 << 8, VIEW_CHANNEL: 1 << 10, SEND_MESSAGES: 1 << 11, From c10b4feeeb7705903593ba134df128c57a823ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20H=C3=AEncu?= Date: Tue, 7 Aug 2018 19:08:49 +0300 Subject: [PATCH 0762/1359] fix(Collection): use `new this.constructor` instead of `new Collection` (#2709) --- src/util/Collection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index 06f54f59c..f8ac733ad 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -220,7 +220,7 @@ class Collection extends Map { */ filter(fn, thisArg) { if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = new Collection(); + const results = new this.constructor(); for (const [key, val] of this) { if (fn(val, key, this)) results.set(key, val); } @@ -237,7 +237,7 @@ class Collection extends Map { */ partition(fn, thisArg) { if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = [new Collection(), new Collection()]; + const results = [new this.constructor(), new this.constructor()]; for (const [key, val] of this) { if (fn(val, key, this)) { results[0].set(key, val); @@ -404,7 +404,7 @@ class Collection extends Map { * @example collection.sort((userA, userB) => userA.createdTimestamp - userB.createdTimestamp); */ sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) { - return new Collection([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); + return new this.constructor([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); } toJSON() { From 46fa9603c21411effee456362c1fe0031ccbfd19 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 7 Aug 2018 17:34:01 +0100 Subject: [PATCH 0763/1359] voice: delete receive stream immediately to prevent it being written to after end (#2678) --- src/client/voice/networking/VoiceWebSocket.js | 5 ++++- src/client/voice/receiver/PacketHandler.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 1e08dedf9..be187761b 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -184,7 +184,10 @@ class VoiceWebSocket extends EventEmitter { case VoiceOPCodes.CLIENT_DISCONNECT: for (const receiver of this.connection.receivers) { const streamInfo = receiver.packets.streams.get(packet.d.user_id); - if (streamInfo) streamInfo.stream.push(null); + if (streamInfo) { + receiver.packets.streams.delete(packet.d.user_id); + streamInfo.stream.push(null); + } } break; case VoiceOPCodes.SPEAKING: diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 03b5b6f69..bcc151f38 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -17,7 +17,10 @@ class PacketHandler extends EventEmitter { _stoppedSpeaking(userID) { const streamInfo = this.streams.get(userID); - if (streamInfo && streamInfo.end === 'silence') streamInfo.stream.push(null); + if (streamInfo && streamInfo.end === 'silence') { + this.streams.delete(userID); + streamInfo.stream.push(null); + } } makeStream(user, end) { From 382afee436ed0bbd56bafa7bf307e73dd82099f9 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 7 Aug 2018 18:25:42 +0100 Subject: [PATCH 0764/1359] deps: update prism peer dep to point to right account --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 11bfcbedf..8287ade82 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "form-data": "^2.3.2", "node-fetch": "^2.1.2", "pako": "^1.0.0", - "prism-media": "hydrabolt/prism-media", + "prism-media": "amishshah/prism-media", "tweetnacl": "^1.0.0", "ws": "^6.0.0" }, From 98dc2df6d4c662d93f2db564f16c4aa4f861a920 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 7 Aug 2018 19:08:27 +0100 Subject: [PATCH 0765/1359] Revert "fix(Collection): use `new this.constructor` instead of `new Collection` (#2709)" This reverts commit c10b4feeeb7705903593ba134df128c57a823ff1. --- src/util/Collection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index f8ac733ad..06f54f59c 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -220,7 +220,7 @@ class Collection extends Map { */ filter(fn, thisArg) { if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = new this.constructor(); + const results = new Collection(); for (const [key, val] of this) { if (fn(val, key, this)) results.set(key, val); } @@ -237,7 +237,7 @@ class Collection extends Map { */ partition(fn, thisArg) { if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = [new this.constructor(), new this.constructor()]; + const results = [new Collection(), new Collection()]; for (const [key, val] of this) { if (fn(val, key, this)) { results[0].set(key, val); @@ -404,7 +404,7 @@ class Collection extends Map { * @example collection.sort((userA, userB) => userA.createdTimestamp - userB.createdTimestamp); */ sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) { - return new this.constructor([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); + return new Collection([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); } toJSON() { From 47d405e70c804d7433f3499129c795eded1f1716 Mon Sep 17 00:00:00 2001 From: DennisV2809 <37509921+DennisV2809@users.noreply.github.com> Date: Wed, 8 Aug 2018 12:41:35 +0200 Subject: [PATCH 0766/1359] Small typo in documentation of Message.js (#2704) * Update Message.js message.reply() example fix * Update Message.js --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 1b5d677d9..9576ea9f5 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -495,7 +495,7 @@ class Message extends Base { * @example * // Reply to a message * message.reply('Hey, I\'m a reply!') - * .then(msg => console.log(`Sent a reply to ${msg.author.username}`)) + * .then(() => console.log(`Sent a reply to ${message.author.username}`)) * .catch(console.error); */ reply(content, options) { From 5980d04f2b5a70b9712aac763f8209ade202e883 Mon Sep 17 00:00:00 2001 From: Crawl Date: Wed, 8 Aug 2018 12:48:34 +0200 Subject: [PATCH 0767/1359] docs: implement #2707 on master --- src/structures/MessageEmbed.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 81706c2dd..72e22464e 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -12,7 +12,11 @@ class MessageEmbed { setup(data) { // eslint-disable-line complexity /** - * The type of this embed + * The type of this embed, either: + * * `image` - an image embed + * * `video` - a video embed + * * `link` - a link embed + * * `rich` - a rich embed * @type {string} */ this.type = data.type; @@ -36,7 +40,7 @@ class MessageEmbed { this.url = data.url; /** - * The color of the embed + * The color of this embed * @type {?number} */ this.color = data.color; From 8152841bab23eb37b2c83c4d2dc136ddc0c01cb4 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 8 Aug 2018 12:45:49 +0100 Subject: [PATCH 0768/1359] Try to cache members from data in message payloads --- src/structures/Message.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/structures/Message.js b/src/structures/Message.js index 9576ea9f5..4307d2244 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -163,6 +163,10 @@ class Message extends Base { * @private */ this._edits = []; + + if (data.member && this.guild && this.author) { + this.guild.members.add(Object.assign(data.member, { user: this.author })); + } } /** From 9dfcb61a90e89051b09b77384726001175a9e56c Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Wed, 8 Aug 2018 13:03:29 -0500 Subject: [PATCH 0769/1359] Revert "fix: Shards not receiving regular messages" This reverts commit 88616eaf3e1854d1ce2f787658059f1617e8f34f. --- src/sharding/ShardClientUtil.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 1ca30bb34..07c282ebf 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -132,8 +132,6 @@ class ShardClientUtil { } catch (err) { this._respond('eval', { _eval: message._eval, _error: Util.makePlainError(err) }); } - } else { - this.send(message); } } From 55863efa155fb504d2ecd06986470eaa6c221f39 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 9 Aug 2018 13:00:46 +0100 Subject: [PATCH 0770/1359] voice: don't clear reject timeout until connection fully resolved (#2421, #2578) --- src/client/voice/VoiceConnection.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 324b113a7..8adbbc740 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -234,7 +234,6 @@ class VoiceConnection extends EventEmitter { const { token, endpoint, sessionID } = this.authentication; if (token && endpoint && sessionID) { - clearTimeout(this.connectTimeout); this.status = VoiceStatus.CONNECTING; /** * Emitted when we successfully initiate a voice connection. From 8e0ea9aa163983977c4cd8a1e1eca490612eb0cf Mon Sep 17 00:00:00 2001 From: 1Computer1 Date: Thu, 9 Aug 2018 08:25:07 -0400 Subject: [PATCH 0771/1359] Collection debug methods, remove deleteAll (#2577) --- src/util/Collection.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index 06f54f59c..86b4e5aa9 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -333,12 +333,29 @@ class Collection extends Map { * @returns {Collection} * @example * collection - * .tap(user => console.log(user.username)) + * .each(user => console.log(user.username)) * .filter(user => user.bot) - * .tap(user => console.log(user.username)); + * .each(user => console.log(user.username)); + */ + each(fn, thisArg) { + this.forEach(fn, thisArg); + return this; + } + + /** + * Runs a function on the collection and returns the collection. + * @param {Function} fn Function to execute + * @param {*} [thisArg] Value to use as `this` when executing function + * @returns {Collection} + * @example + * collection + * .tap(coll => coll.size) + * .filter(user => user.bot) + * .tap(coll => coll.size) */ tap(fn, thisArg) { - this.forEach(fn, thisArg); + if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); + fn(this); return this; } @@ -365,18 +382,6 @@ class Collection extends Map { return newColl; } - /** - * Calls the `delete()` method on all items that have it. - * @returns {Promise[]} - */ - deleteAll() { - const returns = []; - for (const item of this.values()) { - if (item.delete) returns.push(item.delete()); - } - return returns; - } - /** * Checks if this collection shares identical key-value pairings with another. * This is different to checking for equality using equal-signs, because From f3d7f7c3bd662d84cd9fb1780ebebeafa91a2b64 Mon Sep 17 00:00:00 2001 From: Isabella Date: Thu, 9 Aug 2018 07:27:05 -0500 Subject: [PATCH 0772/1359] refactor: move Message#cleanContent function to Util (#2703) * refactor: move Message#cleanContent function to Util * suggested changes --- src/structures/Message.js | 30 +----------------------------- src/util/Util.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 4307d2244..7137b6266 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -257,35 +257,7 @@ class Message extends Base { * @readonly */ get cleanContent() { - return this.content - .replace(/@(everyone|here)/g, '@\u200b$1') - .replace(/<@!?[0-9]+>/g, input => { - const id = input.replace(/<|!|>|@/g, ''); - if (this.channel.type === 'dm' || this.channel.type === 'group') { - return this.client.users.has(id) ? `@${this.client.users.get(id).username}` : input; - } - - const member = this.channel.guild.members.get(id); - if (member) { - if (member.nickname) return `@${member.nickname}`; - return `@${member.user.username}`; - } else { - const user = this.client.users.get(id); - if (user) return `@${user.username}`; - return input; - } - }) - .replace(/<#[0-9]+>/g, input => { - const channel = this.client.channels.get(input.replace(/<|#|>/g, '')); - if (channel) return `#${channel.name}`; - return input; - }) - .replace(/<@&[0-9]+>/g, input => { - if (this.channel.type === 'dm' || this.channel.type === 'group') return input; - const role = this.guild.roles.get(input.replace(/<|@|>|&/g, '')); - if (role) return `@${role.name}`; - return input; - }); + return Util.cleanContent(this.content, this); } /** diff --git a/src/util/Util.js b/src/util/Util.js index ea99f5328..154903486 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -382,6 +382,41 @@ class Util { return dec; } + /** + * The content to have all mentions replaced by the equivalent text. + * @param {string} str The string to be converted + * @param {Message} message The message object to reference + * @returns {string} + */ + static cleanContent(str, message) { + return str + .replace(/@(everyone|here)/g, '@\u200b$1') + .replace(/<@!?[0-9]+>/g, input => { + const id = input.replace(/<|!|>|@/g, ''); + if (message.channel.type === 'dm' || message.channel.type === 'group') { + const user = message.client.users.get(id); + return user ? `@${user.username}` : input; + } + + const member = message.channel.guild.members.get(id); + if (member) { + return member.displayName; + } else { + const user = message.client.users.get(id); + return user ? `@${user.username}` : input; + } + }) + .replace(/<#[0-9]+>/g, input => { + const channel = message.client.channels.get(input.replace(/<|#|>/g, '')); + return channel ? `#${channel.name}` : input; + }) + .replace(/<@&[0-9]+>/g, input => { + if (message.channel.type === 'dm' || message.channel.type === 'group') return input; + const role = message.guild.roles.get(input.replace(/<|@|>|&/g, '')); + return role ? `@${role.name}` : input; + }); + } + /** * Creates a Promise that resolves after a specified duration. * @param {number} ms How long to wait before resolving (in milliseconds) From ee6c19ca7e297d5ffd818c784b2dd980c2bfc804 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 9 Aug 2018 13:59:52 +0100 Subject: [PATCH 0773/1359] voice: rewrite GuildMember#speaking tracking (#2540) --- src/client/voice/VoiceConnection.js | 29 +++++++++++++++++++++++++---- src/structures/Guild.js | 21 --------------------- src/structures/GuildMember.js | 19 ++++++++++++------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 8adbbc740..33f72a125 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -1,7 +1,7 @@ const VoiceWebSocket = require('./networking/VoiceWebSocket'); const VoiceUDP = require('./networking/VoiceUDPClient'); const Util = require('../../util/Util'); -const { OPCodes, VoiceOPCodes, VoiceStatus } = require('../../util/Constants'); +const { OPCodes, VoiceOPCodes, VoiceStatus, Events } = require('../../util/Constants'); const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/Receiver'); const EventEmitter = require('events'); @@ -94,12 +94,19 @@ class VoiceConnection extends EventEmitter { this.once('closing', () => this.player.destroy()); /** - * Map SSRC to speaking values - * @type {Map} + * Map SSRC values to user IDs + * @type {Map} * @private */ this.ssrcMap = new Map(); + /** + * Tracks which users are talking + * @type {Map} + * @private + */ + this._speaking = new Map(); + /** * Object that wraps contains the `ws` and `udp` sockets of this voice connection * @type {Object} @@ -431,6 +438,8 @@ class VoiceConnection extends EventEmitter { const guild = this.channel.guild; const user = this.client.users.get(user_id); this.ssrcMap.set(+ssrc, user_id); + const old = this._speaking.get(user_id); + this._speaking.set(user_id, speaking); /** * Emitted whenever a user starts/stops speaking. * @event VoiceConnection#speaking @@ -445,7 +454,19 @@ class VoiceConnection extends EventEmitter { } } } - guild._memberSpeakUpdate(user_id, speaking); + + if (guild && user && old !== speaking) { + const member = guild.member(user); + if (member) { + /** + * Emitted once a guild member starts/stops speaking. + * @event Client#guildMemberSpeaking + * @param {GuildMember} member The member that started/stopped speaking + * @param {boolean} speaking Whether or not the member is speaking + */ + this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); + } + } } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 184513159..eddfa52e2 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -879,26 +879,6 @@ class Guild extends Base { 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) { - member.speaking = speaking; - /** - * Emitted once a guild member starts/stops speaking. - * @event Client#guildMemberSpeaking - * @param {GuildMember} member The member that started/stopped speaking - * @param {boolean} speaking Whether or not the member is speaking - */ - this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); - } - } } // TODO: Document this thing @@ -914,7 +894,6 @@ class VoiceStateCollection extends Collection { if (member.voiceChannel && member.voiceChannel.id !== voiceState.channel_id) { member.voiceChannel.members.delete(member.id); } - if (!voiceState.channel_id) member.speaking = null; const newChannel = this.guild.channels.get(voiceState.channel_id); if (newChannel) newChannel.members.set(member.user.id, member); } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index e9c5dc663..ea4ddab8e 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -56,14 +56,19 @@ class GuildMember extends Base { if (data) this._patch(data); } - _patch(data) { - /** - * Whether this member is speaking and the client is in the same channel - * @type {boolean} - * @name GuildMember#speaking - */ - if (typeof this.speaking === 'undefined') this.speaking = false; + /** + * Whether this member is speaking. If the client isn't sure, then this will be undefined. Otherwise it will be + * true/false + * @type {?boolean} + * @name GuildMember#speaking + */ + get speaking() { + return this.voiceChannel && this.voiceChannel.connection ? + Boolean(this.voiceChannel.connection._speaking.get(this.id)) : + undefined; + } + _patch(data) { /** * The nickname of this member, if they have one * @type {?string} From be56087c23eff3ee0f5223409a328cc5fcd77c6e Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 9 Aug 2018 14:00:33 +0100 Subject: [PATCH 0774/1359] lint error --- 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 eddfa52e2..050feb7c2 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -2,7 +2,7 @@ const Invite = require('./Invite'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); -const { ChannelTypes, DefaultMessageNotifications, Events, browser } = require('../util/Constants'); +const { ChannelTypes, DefaultMessageNotifications, browser } = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); From 147488df89d91c908171e843ff962dea9fcda373 Mon Sep 17 00:00:00 2001 From: Yukine Date: Fri, 10 Aug 2018 10:56:07 +0200 Subject: [PATCH 0775/1359] use null for getters (#2723) --- src/structures/GuildMember.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index ea4ddab8e..8046b981b 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -65,7 +65,7 @@ class GuildMember extends Base { get speaking() { return this.voiceChannel && this.voiceChannel.connection ? Boolean(this.voiceChannel.connection._speaking.get(this.id)) : - undefined; + null; } _patch(data) { From 0a212809900e0c62f4f10e454851823d98fe47b2 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 10 Aug 2018 10:57:05 +0200 Subject: [PATCH 0776/1359] add Collection as possible param to GuildChannel#overwritePermissions + wording port from 11.3-dev (#2719) * add Collection as possible param to GuildChannel#overwritePermissions * change PermissionoverwriteOptions desc. default to unset * fix eslint hiccup * fix spelling of snowflake --- src/structures/GuildChannel.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 36a84f529..fcdbab224 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -176,10 +176,11 @@ class GuildChannel extends Channel { .freeze(); } + /* eslint-disable max-len */ /** * Replaces the permission overwrites in this channel. * @param {Object} [options] Options - * @param {Array} [options.overwrites] Permission overwrites + * @param {Array|Collection} [options.overwrites] Permission overwrites * @param {string} [options.reason] Reason for updating the channel overwrites * @returns {Promise} * @example @@ -197,9 +198,10 @@ class GuildChannel extends Channel { return this.edit({ permissionOverwrites: resolvePermissions.call(this, overwrites), reason }) .then(() => this); } + /* eslint-enable max-len */ /** - * An object mapping permission flags to `true` (enabled), `null` (default) or `false` (disabled). + * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). * ```js * { * 'SEND_MESSAGES': true, From ce8a679a14122fd0eaac3fe87a53e3049bebc058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20H=C3=AEncu?= Date: Fri, 10 Aug 2018 11:57:33 +0300 Subject: [PATCH 0777/1359] fix(Collection): use Symbol.species for creating derived collections (#2715) --- src/stores/DataStore.js | 4 ++++ src/util/Collection.js | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 72f86028e..3708893c9 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -46,6 +46,10 @@ class DataStore extends Collection { if (typeof idOrInstance === 'string') return idOrInstance; return null; } + + static get [Symbol.species]() { + return Collection; + } } module.exports = DataStore; diff --git a/src/util/Collection.js b/src/util/Collection.js index 86b4e5aa9..887d999b1 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -220,7 +220,7 @@ class Collection extends Map { */ filter(fn, thisArg) { if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = new Collection(); + const results = new this.constructor[Symbol.species](); for (const [key, val] of this) { if (fn(val, key, this)) results.set(key, val); } @@ -237,7 +237,7 @@ class Collection extends Map { */ partition(fn, thisArg) { if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = [new Collection(), new Collection()]; + const results = [new this.constructor[Symbol.species](), new this.constructor[Symbol.species]()]; for (const [key, val] of this) { if (fn(val, key, this)) { results[0].set(key, val); @@ -365,7 +365,7 @@ class Collection extends Map { * @example const newColl = someColl.clone(); */ clone() { - return new this.constructor(this); + return new this.constructor[Symbol.species](this); } /** @@ -409,7 +409,8 @@ class Collection extends Map { * @example collection.sort((userA, userB) => userA.createdTimestamp - userB.createdTimestamp); */ sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) { - return new Collection([...this.entries()].sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); + return new this.constructor[Symbol.species]([...this.entries()] + .sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); } toJSON() { From 2c8e15e31c23acdf06de88d42a800cb3c4c05592 Mon Sep 17 00:00:00 2001 From: Isabella Date: Fri, 10 Aug 2018 04:08:48 -0500 Subject: [PATCH 0778/1359] docs fix: RoleResolvable typedef missing and incorrecty documented methods (#2640) * docs fix: RoleResolvable typedef missing and incorrecty documented methods * requested changes??? * REAL requested changes * shhh --- src/stores/RoleStore.js | 50 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index c2878968c..3c3a5b35a 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -17,6 +17,31 @@ class RoleStore extends DataStore { return super.add(data, cache, { extras: [this.guild] }); } + /** + * Data that can be resolved to a Role object. This can be: + * * A Role + * * A Snowflake + * @typedef {Role|Snowflake} RoleResolvable + */ + + /** + * Resolves a RoleResolvable to a Role object. + * @method resolve + * @memberof RoleStore + * @instance + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Role} + */ + + /** + * Resolves a RoleResolvable to a role ID string. + * @method resolveID + * @memberof RoleStore + * @instance + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Snowflake} + */ + /** * Creates a new role in the guild with given information. * The position will silently reset to 1 if an invalid one is provided, or none. @@ -63,31 +88,6 @@ class RoleStore extends DataStore { get highest() { return this.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this.first()); } - - /** - * Data that can be resolved to a Role object. This can be: - * * A Role - * * A Snowflake - * @typedef {Role|Snowflake} RoleResolvable - */ - - /** - * Resolves a RoleResolvable to a Role object. - * @method resolve - * @memberof RoleStore - * @instance - * @param {RoleResolvable} role The role resolvable to resolve - * @returns {?Role} - */ - - /** - * Resolves a RoleResolvable to a role ID string. - * @method resolveID - * @memberof RoleStore - * @instance - * @param {RoleResolvable} role The role resolvable to resolve - * @returns {?Snowflake} - */ } module.exports = RoleStore; From 0f63c50c063e072f034ead0d6a94ff31b0e3fd5d Mon Sep 17 00:00:00 2001 From: Kyra Date: Fri, 10 Aug 2018 11:23:22 +0200 Subject: [PATCH 0779/1359] fix: Util.basename being unreliable (#2679) * fix: Util.basename being unreliable new URL for WHATWG parsing was not chosen because it only works for URLs and threw in local pathes, path.basename is unreliable (according to the devs' note), and path.parse seems to work well. * docs: Update Util.basename's description --- src/util/Util.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 154903486..a214695a0 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -2,7 +2,7 @@ const { Colors, DefaultOptions, Endpoints } = require('./Constants'); const fetch = require('node-fetch'); const { Error: DiscordError, RangeError, TypeError } = require('../errors'); const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k); -const splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^/]+?|)(\.[^./]*|))(?:[/]*)$/; +const { parse } = require('path'); /** * Contains various general-purpose utility methods. These functions are also available on the base `Discord` object. @@ -323,16 +323,15 @@ class Util { } /** - * Alternative to Node's `path.basename` that we have for some (probably stupid) reason. + * Alternative to Node's `path.basename`, removing query string after the extension if it exists. * @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)[3]; - if (ext && f.endsWith(ext)) f = f.slice(0, -ext.length); - return f; + let res = parse(path); + return ext && res.ext.startsWith(ext) ? res.name : res.base.split('?')[0]; } /** From ea4375bf9051bebdf25411d2eef096bcf148168b Mon Sep 17 00:00:00 2001 From: NbOpposite Date: Fri, 10 Aug 2018 15:44:32 +0200 Subject: [PATCH 0780/1359] Fixed splitting messages with codeblocks (#2720) --- src/structures/shared/CreateMessage.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js index ee9756f77..8ba66c342 100644 --- a/src/structures/shared/CreateMessage.js +++ b/src/structures/shared/CreateMessage.js @@ -36,25 +36,29 @@ module.exports = async function createMessage(channel, options) { } } + if (options.split && typeof options.split !== 'object') options.split = {}; + let mentionPart = ''; 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 (options.split) options.split.prepend = `${mention}, ${options.split.prepend || ''}`; - content = `${mention}${typeof options.content !== 'undefined' ? `, ${options.content}` : ''}`; + mentionPart = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>, `; + if (options.split) options.split.prepend = `${mentionPart}${options.split.prepend || ''}`; } - if (content) { - options.content = Util.resolveString(content); - if (options.split && typeof options.split !== 'object') options.split = {}; + if (content || mentionPart) { + options.content = Util.resolveString(content || ''); // 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\`\`\``; + options.content = `${mentionPart}\`\`\`${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```'; + options.split.prepend = + `${options.split.prepend || ''}\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n`; + + options.split.append = `\n\`\`\`${options.split.append || ''}`; } + } else if (mentionPart) { + options.content = mentionPart + (options.content || ''); } // Add zero-width spaces to @everyone/@here From be5efea461660c8fa38a57f21019af4b37746d17 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 10 Aug 2018 14:44:59 +0100 Subject: [PATCH 0781/1359] rewrite voice state handling --- docs/topics/voice.md | 4 +- .../packets/handlers/VoiceStateUpdate.js | 19 ++- src/errors/Messages.js | 2 + src/index.js | 1 + src/stores/VoiceStateStore.js | 20 +++ src/structures/Guild.js | 38 +---- src/structures/GuildMember.js | 90 +----------- src/structures/VoiceChannel.js | 23 ++- src/structures/VoiceState.js | 131 ++++++++++++++++++ src/util/Structures.js | 1 + test/voice.js | 2 +- 11 files changed, 187 insertions(+), 144 deletions(-) create mode 100644 src/stores/VoiceStateStore.js create mode 100644 src/structures/VoiceState.js diff --git a/docs/topics/voice.md b/docs/topics/voice.md index d08583495..e48b8b325 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -31,8 +31,8 @@ client.on('message', async message => { if (message.content === '/join') { // Only try to join the sender's voice channel if they are in one themselves - if (message.member.voiceChannel) { - const connection = await message.member.voiceChannel.join(); + if (message.member.voice.channel) { + const connection = await message.member.voice.channel.join(); } else { message.reply('You need to join a voice channel first!'); } diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js index 5b81cc1d7..f76f51d9b 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -9,28 +9,27 @@ class VoiceStateUpdateHandler extends AbstractHandler { const guild = client.guilds.get(data.guild_id); if (guild) { + // Update the state + const oldState = guild.voiceStates.get(data.user_id); + if (oldState) oldState._patch(data); + else guild.voiceStates.add(data); + const member = guild.members.get(data.user_id); if (member) { - const oldMember = member._clone(); - oldMember._frozenVoiceState = oldMember.voiceState; - if (member.user.id === client.user.id && data.channel_id) { client.emit('self.voiceStateUpdate', data); } - - guild.voiceStates.set(member.user.id, data); - - client.emit(Events.VOICE_STATE_UPDATE, oldMember, member); + client.emit(Events.VOICE_STATE_UPDATE, oldState, member.voiceState); } } } } /** - * Emitted whenever a user changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate - * @param {GuildMember} oldMember The member before the voice state update - * @param {GuildMember} newMember The member after the voice state update + * @param {VoiceState} oldState The voice state before the update + * @param {VoiceState} newState The voice state after the update */ module.exports = VoiceStateUpdateHandler; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 97f6af280..86bd54cd1 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -53,6 +53,8 @@ const Messages = { VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type', VOICE_PRISM_DEMUXERS_NEED_STREAM: 'To play a webm/ogg stream, you need to pass a ReadableStream.', + VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', + OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', diff --git a/src/index.js b/src/index.js index aad9c9433..8c7f5fb19 100644 --- a/src/index.js +++ b/src/index.js @@ -83,6 +83,7 @@ module.exports = { UserConnection: require('./structures/UserConnection'), VoiceChannel: require('./structures/VoiceChannel'), VoiceRegion: require('./structures/VoiceRegion'), + VoiceState: require('./structures/VoiceState'), Webhook: require('./structures/Webhook'), WebSocket: require('./WebSocket'), diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js new file mode 100644 index 000000000..bc14c8f4b --- /dev/null +++ b/src/stores/VoiceStateStore.js @@ -0,0 +1,20 @@ +const DataStore = require('./DataStore'); +const VoiceState = require('../structures/VoiceState'); + +class VoiceStateStore extends DataStore { + constructor(guild, iterable) { + super(guild.client, iterable, VoiceState); + this.guild = guild; + } + + add(data, cache = true) { + const existing = this.get(data.user_id); + if (existing) return existing; + + const entry = new VoiceState(this.guild, data); + if (cache) this.set(data.user_id, entry); + return entry; + } +} + +module.exports = VoiceStateStore; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 050feb7c2..260819251 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -12,6 +12,7 @@ const RoleStore = require('../stores/RoleStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); const GuildChannelStore = require('../stores/GuildChannelStore'); const PresenceStore = require('../stores/PresenceStore'); +const VoiceStateStore = require('../stores/VoiceStateStore'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); @@ -229,9 +230,13 @@ class Guild extends Base { } } - if (!this.voiceStates) this.voiceStates = new VoiceStateCollection(this); + if (!this.voiceStates) this.voiceStates = new VoiceStateStore(this); if (data.voice_states) { - for (const voiceState of data.voice_states) this.voiceStates.set(voiceState.user_id, voiceState); + for (const voiceState of data.voice_states) { + const existing = this.voiceStates.get(voiceState.user_id); + if (existing) existing._patch(voiceState); + else this.voiceStates.add(voiceState); + } } if (!this.emojis) { @@ -881,33 +886,4 @@ class Guild extends Base { } } -// 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) { - if (member.voiceChannel && member.voiceChannel.id !== voiceState.channel_id) { - member.voiceChannel.members.delete(member.id); - } - const newChannel = this.guild.channels.get(voiceState.channel_id); - if (newChannel) newChannel.members.set(member.user.id, member); - } - super.set(id, voiceState); - } - - delete(id) { - const voiceState = this.get(id); - if (voiceState && voiceState.channel_id) { - const channel = this.guild.channels.get(voiceState.channel_id); - if (channel) channel.members.delete(id); - } - return super.delete(id); - } -} - module.exports = Guild; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8046b981b..8048481d5 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -56,18 +56,6 @@ class GuildMember extends Base { if (data) this._patch(data); } - /** - * Whether this member is speaking. If the client isn't sure, then this will be undefined. Otherwise it will be - * true/false - * @type {?boolean} - * @name GuildMember#speaking - */ - get speaking() { - return this.voiceChannel && this.voiceChannel.connection ? - Boolean(this.voiceChannel.connection._speaking.get(this.id)) : - null; - } - _patch(data) { /** * The nickname of this member, if they have one @@ -107,52 +95,10 @@ class GuildMember extends Base { return (channel && channel.messages.get(this.lastMessageID)) || null; } - get voiceState() { - return this._frozenVoiceState || this.guild.voiceStates.get(this.id) || {}; + get voice() { + return this.guild.voiceStates.get(this.id); } - /** - * 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; } - /** * The time this member joined the guild * @type {?Date} @@ -191,33 +137,6 @@ class GuildMember extends Base { return (role && role.hexColor) || '#000000'; } - /** - * Whether this member is muted in any way - * @type {boolean} - * @readonly - */ - get mute() { - return this.selfMute || this.serverMute; - } - - /** - * Whether this member is deafened in any way - * @type {boolean} - * @readonly - */ - get deaf() { - return this.selfDeaf || this.serverDeaf; - } - - /** - * The voice channel this member is in, if any - * @type {?VoiceChannel} - * @readonly - */ - get voiceChannel() { - return this.guild.channels.get(this.voiceChannelID) || null; - } - /** * The ID of this member * @type {Snowflake} @@ -344,11 +263,6 @@ class GuildMember extends Base { const clone = this._clone(); data.user = this.user; clone._patch(data); - clone._frozenVoiceState = {}; - Object.assign(clone._frozenVoiceState, this.voiceState); - if (typeof data.mute !== 'undefined') clone._frozenVoiceState.mute = data.mute; - if (typeof data.deaf !== 'undefined') clone._frozenVoiceState.mute = data.deaf; - if (typeof data.channel_id !== 'undefined') clone._frozenVoiceState.channel_id = data.channel_id; return clone; }); } diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 716440051..c8b431076 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,5 +1,4 @@ const GuildChannel = require('./GuildChannel'); -const Collection = require('../util/Collection'); const { browser } = require('../util/Constants'); const Permissions = require('../util/Permissions'); const { Error } = require('../errors'); @@ -9,17 +8,6 @@ const { Error } = require('../errors'); * @extends {GuildChannel} */ class VoiceChannel extends GuildChannel { - constructor(guild, data) { - super(guild, data); - - /** - * The members in this voice channel - * @type {Collection} - * @name VoiceChannel#members - */ - Object.defineProperty(this, 'members', { value: new Collection() }); - } - _patch(data) { super._patch(data); /** @@ -35,6 +23,17 @@ class VoiceChannel extends GuildChannel { this.userLimit = data.user_limit; } + /** + * The members in this voice channel + * @type {Collection} + * @name VoiceChannel#members + */ + get members() { + return this.guild.voiceStates + .filter(state => state.channelID === this.id && state.member) + .map(state => state.member); + } + /** * The voice connection for this voice channel, if the client is connected * @type {?VoiceConnection} diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js new file mode 100644 index 000000000..faafc62c1 --- /dev/null +++ b/src/structures/VoiceState.js @@ -0,0 +1,131 @@ +const Base = require('./Base'); + +/** + * Represents the voice state for a Guild Member. + */ +class VoiceState extends Base { + constructor(guild, data) { + super(guild.client); + /** + * The guild of this voice state + * @type {Guild} + */ + this.guild = guild; + /** + * The ID of the member of this voice state + * @type {Snowflake} + */ + this.id = data.user_id; + this._patch(data); + } + + _patch(data) { + /** + * Whether this member is deafened server-wide + * @type {boolean} + */ + this.serverDeaf = data.deaf; + /** + * Whether this member is muted server-wide + * @type {boolean} + */ + this.serverMute = data.mute; + /** + * Whether this member is self-deafened + * @type {boolean} + */ + this.selfDeaf = data.self_deaf; + /** + * Whether this member is self-muted + * @type {boolean} + */ + this.selfMute = data.self_mute; + /** + * The session ID of this member's connection + * @type {String} + */ + this.sessionID = data.session_id; + /** + * The ID of the voice channel that this member is in + * @type {Snowflake} + */ + this.channelID = data.channel_id; + } + + /** + * The member that this voice state belongs to + * @type {GuildMember} + */ + get member() { + return this.guild.members.get(this.id); + } + + /** + * The channel that the member is connected to + * @type {VoiceChannel} + */ + get channel() { + return this.guild.channels.get(this.channelID); + } + + /** + * Whether this member is either self-deafened or server-deafened + * @type {boolean} + */ + get deaf() { + return this.serverDeaf || this.selfDeaf; + } + + /** + * Whether this member is either self-muted or server-muted + * @type {boolean} + */ + get mute() { + return this.serverMute || this.selfMute; + } + + /** + * Whether this member is currently speaking. A boolean if the information is available (aka + * the bot is connected to any voice channel in the guild), otherwise this is null + * @type {boolean|null} + */ + get speaking() { + return this.channel && this.channel.connection ? + Boolean(this.channel.connection._speaking.get(this.id)) : + null; + } + + /** + * Mutes/unmutes the member of this voice state. + * @param {boolean} mute Whether or not the member should be muted + * @param {string} [reason] Reason for muting or unmuting + * @returns {Promise} + */ + setMute(mute, reason) { + return this.member ? this.member.edit({ mute }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + + /** + * Deafens/undeafens the member of this voice state. + * @param {boolean} deaf Whether or not the member should be deafened + * @param {string} [reason] Reason for deafening or undeafening + * @returns {Promise} + */ + setDeaf(deaf, reason) { + return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + + toJSON() { + return super.toJSON({ + id: true, + serverDeaf: true, + serverMute: true, + selfDeaf: true, + selfMute: true, + sessionID: true, + channelID: 'channel', + }); + } +} + +module.exports = VoiceState; diff --git a/src/util/Structures.js b/src/util/Structures.js index e7f615c79..c81235033 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -73,6 +73,7 @@ const structures = { Message: require('../structures/Message'), MessageReaction: require('../structures/MessageReaction'), Presence: require('../structures/Presence').Presence, + VoiceState: require('../structures/VoiceState'), Role: require('../structures/Role'), User: require('../structures/User'), }; diff --git a/test/voice.js b/test/voice.js index 07cc3a4bf..8494425ca 100644 --- a/test/voice.js +++ b/test/voice.js @@ -33,7 +33,7 @@ client.on('message', m => { if (!m.guild) return; if (m.author.id !== '66564597481480192') return; if (m.content.startsWith('/join')) { - const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voiceChannel; + const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voice.channel; if (channel && channel.type === 'voice') { channel.join().then(conn => { const receiver = conn.createReceiver(); From fe8ece0192d8057cac3f02e7fab467238f9022af Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 10 Aug 2018 15:15:52 +0100 Subject: [PATCH 0782/1359] voice state fixes --- .../websocket/packets/handlers/VoiceStateUpdate.js | 11 +++++------ src/stores/VoiceStateStore.js | 2 +- src/structures/Guild.js | 4 +--- src/structures/VoiceState.js | 1 + test/voice.js | 4 ++++ 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js index f76f51d9b..97b32e816 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -10,16 +10,15 @@ class VoiceStateUpdateHandler extends AbstractHandler { const guild = client.guilds.get(data.guild_id); if (guild) { // Update the state - const oldState = guild.voiceStates.get(data.user_id); - if (oldState) oldState._patch(data); - else guild.voiceStates.add(data); - + let oldState = guild.voiceStates.get(data.user_id); + if (oldState) oldState = oldState._clone(); + const newState = guild.voiceStates.add(data); const member = guild.members.get(data.user_id); if (member) { if (member.user.id === client.user.id && data.channel_id) { client.emit('self.voiceStateUpdate', data); } - client.emit(Events.VOICE_STATE_UPDATE, oldState, member.voiceState); + client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); } } } @@ -28,7 +27,7 @@ class VoiceStateUpdateHandler extends AbstractHandler { /** * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate - * @param {VoiceState} oldState The voice state before the update + * @param {?VoiceState} oldState The voice state before the update * @param {VoiceState} newState The voice state after the update */ diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js index bc14c8f4b..2de492495 100644 --- a/src/stores/VoiceStateStore.js +++ b/src/stores/VoiceStateStore.js @@ -9,7 +9,7 @@ class VoiceStateStore extends DataStore { add(data, cache = true) { const existing = this.get(data.user_id); - if (existing) return existing; + if (existing) return existing._patch(data); const entry = new VoiceState(this.guild, data); if (cache) this.set(data.user_id, entry); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 260819251..bfb09d9f5 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -233,9 +233,7 @@ class Guild extends Base { if (!this.voiceStates) this.voiceStates = new VoiceStateStore(this); if (data.voice_states) { for (const voiceState of data.voice_states) { - const existing = this.voiceStates.get(voiceState.user_id); - if (existing) existing._patch(voiceState); - else this.voiceStates.add(voiceState); + this.voiceStates.add(voiceState); } } diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index faafc62c1..098ec9cff 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -50,6 +50,7 @@ class VoiceState extends Base { * @type {Snowflake} */ this.channelID = data.channel_id; + return this; } /** diff --git a/test/voice.js b/test/voice.js index 8494425ca..d516f21a4 100644 --- a/test/voice.js +++ b/test/voice.js @@ -29,6 +29,10 @@ var count = 0; process.on('unhandledRejection', console.log); +client.on('voiceStateUpdate', (a, b) => { + console.log(a ? a.channelID : null, b ? b.channelID : null, b.member.user.username); +}); + client.on('message', m => { if (!m.guild) return; if (m.author.id !== '66564597481480192') return; From 08eff6693971ed57f96f32b8b41e3969ff0cd325 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 10 Aug 2018 16:46:14 +0100 Subject: [PATCH 0783/1359] Make presences track users and guilds, emit them in presenceUpdate --- .../packets/handlers/PresenceUpdate.js | 14 ++++++-------- src/client/websocket/packets/handlers/Ready.js | 2 +- src/stores/ClientPresenceStore.js | 2 ++ src/structures/Guild.js | 2 +- src/structures/GuildMember.js | 2 +- src/structures/Presence.js | 18 ++++++++++++++++++ test/voice.js | 5 +++-- 7 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js index cb6935a9a..d223c278a 100644 --- a/src/client/websocket/packets/handlers/PresenceUpdate.js +++ b/src/client/websocket/packets/handlers/PresenceUpdate.js @@ -23,6 +23,8 @@ class PresenceUpdateHandler extends AbstractHandler { } if (guild) { + let oldPresence = guild.presences.get(user.id); + if (oldPresence) oldPresence = oldPresence._clone(); let member = guild.members.get(user.id); if (!member && data.status !== 'offline') { member = guild.members.add({ @@ -35,17 +37,13 @@ class PresenceUpdateHandler extends AbstractHandler { } if (member) { if (client.listenerCount(Events.PRESENCE_UPDATE) === 0) { - guild.presences.add(data); + guild.presences.add(Object.assign(data, { guild: this })); return; } - const oldMember = member._clone(); - if (member.presence) { - oldMember.frozenPresence = member.presence._clone(); - } - guild.presences.add(data); - client.emit(Events.PRESENCE_UPDATE, oldMember, member); + guild.presences.add(Object.assign(data, { guild: this })); + client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); } else { - guild.presences.add(data); + guild.presences.add(Object.assign(data, { guild: this })); } } } diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index 0e48e5586..59b872d90 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -9,6 +9,7 @@ class ReadyHandler extends AbstractHandler { client.ws.heartbeat(); + client.presences.clientPresence.userID = data.user.id; if (!ClientUser) ClientUser = require('../../../../structures/ClientUser'); const clientUser = new ClientUser(client, data.user); client.user = clientUser; @@ -17,7 +18,6 @@ class ReadyHandler extends AbstractHandler { for (const guild of data.guilds) client.guilds.add(guild); for (const privateDM of data.private_channels) client.channels.add(privateDM); - for (const presence of data.presences || []) client.presences.add(presence); if (!client.users.has('1')) { client.users.add({ diff --git a/src/stores/ClientPresenceStore.js b/src/stores/ClientPresenceStore.js index 94d48d4a4..8e6336d6c 100644 --- a/src/stores/ClientPresenceStore.js +++ b/src/stores/ClientPresenceStore.js @@ -16,6 +16,8 @@ class ClientPresenceStore extends PresenceStore { afk: false, since: null, activity: null, + user: { id: null }, + guild_id: null, }); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index bfb09d9f5..423083e93 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -226,7 +226,7 @@ class Guild extends Base { if (data.presences) { for (const presence of data.presences) { - this.presences.add(presence); + this.presences.add(Object.assign(presence, { guild: this })); } } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8048481d5..8a314c086 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -114,7 +114,7 @@ class GuildMember extends Base { * @readonly */ get presence() { - return this.frozenPresence || this.guild.presences.get(this.id) || new Presence(this.client); + return this.guild.presences.get(this.id) || new Presence(this.client); } /** diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 3a93af628..13d3ff972 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -14,9 +14,27 @@ const { ActivityTypes, ActivityFlags } = require('../util/Constants'); class Presence { constructor(client, data = {}) { Object.defineProperty(this, 'client', { value: client }); + this.userID = data.user.id; + this.guild = data.guild; this.patch(data); } + /** + * The user of this presence + * @type {User} + */ + get user() { + return this.client.users.get(this.userID); + } + + /** + * The member of this presence + * @type {GuildMember} + */ + get member() { + return this.guild.members.get(this.userID); + } + patch(data) { /** * The status of the presence: diff --git a/test/voice.js b/test/voice.js index d516f21a4..b4eac0128 100644 --- a/test/voice.js +++ b/test/voice.js @@ -29,8 +29,9 @@ var count = 0; process.on('unhandledRejection', console.log); -client.on('voiceStateUpdate', (a, b) => { - console.log(a ? a.channelID : null, b ? b.channelID : null, b.member.user.username); +client.on('presenceUpdate', (a, b) => { + if (b.userID !== '66564597481480192') return; + console.log(a ? a.status : null, b.status, b.user.username); }); client.on('message', m => { From 00ac62f9752f9d433d2b0bab50ee66030d1e8a6a Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 10 Aug 2018 16:59:01 +0100 Subject: [PATCH 0784/1359] chore: update typings --- typings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings b/typings index b1127ec10..db2a6e498 160000 --- a/typings +++ b/typings @@ -1 +1 @@ -Subproject commit b1127ec10ee951158dcc7220fe37a6e005806a1d +Subproject commit db2a6e498bc625087cdd873a4af637539c4d4cfd From e059581eee17096e41aed923e79d023f84f46a21 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 10 Aug 2018 17:05:26 +0100 Subject: [PATCH 0785/1359] GuildMember#voice never undefined, improve documentation for VoiceState --- src/structures/GuildMember.js | 3 ++- src/structures/VoiceState.js | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8a314c086..79f528ac5 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -3,6 +3,7 @@ const Role = require('./Role'); const Permissions = require('../util/Permissions'); const GuildMemberRoleStore = require('../stores/GuildMemberRoleStore'); const Base = require('./Base'); +const VoiceState = require('./VoiceState'); const { Presence } = require('./Presence'); const { Error } = require('../errors'); @@ -96,7 +97,7 @@ class GuildMember extends Base { } get voice() { - return this.guild.voiceStates.get(this.id); + return this.guild.voiceStates.get(this.id) || new VoiceState(this.guild, { user_id: this.id }); } /** diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 098ec9cff..ebe104747 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -22,32 +22,32 @@ class VoiceState extends Base { _patch(data) { /** * Whether this member is deafened server-wide - * @type {boolean} + * @type {?boolean} */ this.serverDeaf = data.deaf; /** * Whether this member is muted server-wide - * @type {boolean} + * @type {?boolean} */ this.serverMute = data.mute; /** * Whether this member is self-deafened - * @type {boolean} + * @type {?boolean} */ this.selfDeaf = data.self_deaf; /** * Whether this member is self-muted - * @type {boolean} + * @type {?boolean} */ this.selfMute = data.self_mute; /** * The session ID of this member's connection - * @type {String} + * @type {?string} */ this.sessionID = data.session_id; /** * The ID of the voice channel that this member is in - * @type {Snowflake} + * @type {?Snowflake} */ this.channelID = data.channel_id; return this; @@ -55,7 +55,7 @@ class VoiceState extends Base { /** * The member that this voice state belongs to - * @type {GuildMember} + * @type {?GuildMember} */ get member() { return this.guild.members.get(this.id); @@ -63,7 +63,7 @@ class VoiceState extends Base { /** * The channel that the member is connected to - * @type {VoiceChannel} + * @type {?VoiceChannel} */ get channel() { return this.guild.channels.get(this.channelID); @@ -71,7 +71,7 @@ class VoiceState extends Base { /** * Whether this member is either self-deafened or server-deafened - * @type {boolean} + * @type {?boolean} */ get deaf() { return this.serverDeaf || this.selfDeaf; @@ -79,7 +79,7 @@ class VoiceState extends Base { /** * Whether this member is either self-muted or server-muted - * @type {boolean} + * @type {?boolean} */ get mute() { return this.serverMute || this.selfMute; @@ -88,7 +88,7 @@ class VoiceState extends Base { /** * Whether this member is currently speaking. A boolean if the information is available (aka * the bot is connected to any voice channel in the guild), otherwise this is null - * @type {boolean|null} + * @type {?boolean} */ get speaking() { return this.channel && this.channel.connection ? From 34ed3c6014ae26977910f3936c2f9aa44196a444 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Fri, 10 Aug 2018 17:09:05 +0100 Subject: [PATCH 0786/1359] Update documentation for presenceUpdate --- src/client/websocket/packets/handlers/PresenceUpdate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js index d223c278a..aa4f8768d 100644 --- a/src/client/websocket/packets/handlers/PresenceUpdate.js +++ b/src/client/websocket/packets/handlers/PresenceUpdate.js @@ -52,8 +52,8 @@ class PresenceUpdateHandler extends AbstractHandler { /** * Emitted whenever a guild member's presence (e.g. status, activity) is changed. * @event Client#presenceUpdate - * @param {GuildMember} oldMember The member before the presence update - * @param {GuildMember} newMember The member after the presence update + * @param {?Presence} oldPresence The presence before the update, if one at all + * @param {Presence} newPresence The presence after the update */ /** From f5ea673ebd01677aded6adf3edc723fb0535d95e Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 11 Aug 2018 00:26:24 +0100 Subject: [PATCH 0787/1359] fix default presence --- src/structures/GuildMember.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 79f528ac5..d24670b64 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -115,7 +115,7 @@ class GuildMember extends Base { * @readonly */ get presence() { - return this.guild.presences.get(this.id) || new Presence(this.client); + return this.guild.presences.get(this.id) || new Presence(this.client, { user: { id: this.id } }); } /** From 3c2eaff22655fe33d8de3bed410b4b3b08fe4c9d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 11 Aug 2018 10:19:31 +0100 Subject: [PATCH 0788/1359] fix default presences again, remove redundant extras in PresenceStore --- src/stores/PresenceStore.js | 2 +- src/structures/GuildMember.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index 15fc9f124..dc758d3c3 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -12,7 +12,7 @@ class PresenceStore extends DataStore { add(data, cache) { const existing = this.get(data.user.id); - return existing ? existing.patch(data) : super.add(data, cache, { id: data.user.id }); + return existing ? existing.patch(data) : super.add(data, cache); } /** diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d24670b64..9b6beee74 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -115,7 +115,12 @@ class GuildMember extends Base { * @readonly */ get presence() { - return this.guild.presences.get(this.id) || new Presence(this.client, { user: { id: this.id } }); + return this.guild.presences.get(this.id) || new Presence(this.client, { + user: { + id: this.id, + }, + guild: this, + }); } /** From ea764afad2cbdc0ee4a82af118b6c7b4182af119 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 11 Aug 2018 10:46:51 +0100 Subject: [PATCH 0789/1359] add ClientPresence, remove ClientPresenceStore --- src/client/Client.js | 9 +++--- src/client/ClientManager.js | 4 +-- .../websocket/packets/handlers/Ready.js | 2 +- src/index.js | 2 +- .../ClientPresence.js} | 31 ++++++------------- src/structures/ClientUser.js | 4 +-- src/structures/User.js | 1 - src/util/Structures.js | 1 + 8 files changed, 21 insertions(+), 33 deletions(-) rename src/{stores/ClientPresenceStore.js => structures/ClientPresence.js} (76%) diff --git a/src/client/Client.js b/src/client/Client.js index 5b03c4435..a193587bd 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -14,7 +14,7 @@ const VoiceBroadcast = require('./voice/VoiceBroadcast'); const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); -const ClientPresenceStore = require('../stores/ClientPresenceStore'); +const ClientPresence = require('../structures/ClientPresence'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); const { Events, browser } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); @@ -97,10 +97,11 @@ class Client extends BaseClient { this.channels = new ChannelStore(this); /** - * Presences that have been received for the client user, mapped by user IDs - * @type {ClientPresenceStore} + * The presence of the Client + * @private + * @type {ClientPresence} */ - this.presences = new ClientPresenceStore(this); + this.presence = new ClientPresence(this); Object.defineProperty(this, 'token', { writable: true }); if (!browser && !this.token && 'CLIENT_TOKEN' in process.env) { diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index b283fd780..e6d107822 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -41,9 +41,9 @@ class ClientManager { const timeout = this.client.setTimeout(() => reject(new Error('WS_CONNECTION_TIMEOUT')), 1000 * 300); this.client.api.gateway.get().then(async res => { if (this.client.options.presence != null) { // eslint-disable-line eqeqeq - const presence = await this.client.presences._parse(this.client.options.presence); + const presence = await this.client.presence._parse(this.client.options.presence); this.client.options.ws.presence = presence; - this.client.presences.clientPresence.patch(presence); + this.client.presence.patch(presence); } const gateway = `${res.url}/`; this.client.emit(Events.DEBUG, `Using gateway ${gateway}`); diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index 59b872d90..c1a16df02 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -9,7 +9,7 @@ class ReadyHandler extends AbstractHandler { client.ws.heartbeat(); - client.presences.clientPresence.userID = data.user.id; + client.presence.userID = data.user.id; if (!ClientUser) ClientUser = require('../../../../structures/ClientUser'); const clientUser = new ClientUser(client, data.user); client.user = clientUser; diff --git a/src/index.js b/src/index.js index 8c7f5fb19..5ed3da570 100644 --- a/src/index.js +++ b/src/index.js @@ -25,7 +25,6 @@ module.exports = { // Stores ChannelStore: require('./stores/ChannelStore'), - ClientPresenceStore: require('./stores/ClientPresenceStore'), GuildChannelStore: require('./stores/GuildChannelStore'), GuildEmojiStore: require('./stores/GuildEmojiStore'), GuildEmojiRoleStore: require('./stores/GuildEmojiRoleStore'), @@ -74,6 +73,7 @@ module.exports = { MessageReaction: require('./structures/MessageReaction'), PermissionOverwrites: require('./structures/PermissionOverwrites'), Presence: require('./structures/Presence').Presence, + ClientPresence: require('./structures/ClientPresence'), ReactionCollector: require('./structures/ReactionCollector'), ReactionEmoji: require('./structures/ReactionEmoji'), RichPresenceAssets: require('./structures/Presence').RichPresenceAssets, diff --git a/src/stores/ClientPresenceStore.js b/src/structures/ClientPresence.js similarity index 76% rename from src/stores/ClientPresenceStore.js rename to src/structures/ClientPresence.js index 8e6336d6c..adba78f3e 100644 --- a/src/stores/ClientPresenceStore.js +++ b/src/structures/ClientPresence.js @@ -1,31 +1,18 @@ -const PresenceStore = require('./PresenceStore'); +const { Presence } = require('./Presence'); const Collection = require('../util/Collection'); const { ActivityTypes, OPCodes } = require('../util/Constants'); -const { Presence } = require('../structures/Presence'); const { TypeError } = require('../errors'); -/** - * Stores the client presence and other presences. - * @extends {PresenceStore} - */ -class ClientPresenceStore extends PresenceStore { - constructor(...args) { - super(...args); - this.clientPresence = new Presence(this.client, { - status: 'online', - afk: false, - since: null, - activity: null, - user: { id: null }, - guild_id: null, - }); +class ClientPresence extends Presence { + constructor(client, data = {}) { + super(client, Object.assign(data, { status: 'online', user: { id: null } })); } async setClientPresence(presence) { const packet = await this._parse(presence); - this.clientPresence.patch(packet); + this.patch(packet); this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet }); - return this.clientPresence; + return this; } async _parse({ status, since, afk, activity }) { // eslint-disable-line complexity @@ -45,7 +32,7 @@ class ClientPresenceStore extends PresenceStore { const packet = { afk: afk != null ? afk : false, // eslint-disable-line eqeqeq since: since != null ? since : null, // eslint-disable-line eqeqeq - status: status || this.clientPresence.status, + status: status || this.status, game: activity ? { type: activity.type, name: activity.name, @@ -67,7 +54,7 @@ class ClientPresenceStore extends PresenceStore { }; if ((status || afk || since) && !activity) { - packet.game = this.clientPresence.activity; + packet.game = this.activity; } if (packet.game) { @@ -79,4 +66,4 @@ class ClientPresenceStore extends PresenceStore { } } -module.exports = ClientPresenceStore; +module.exports = ClientPresence; diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 31d966e6d..8d35765d9 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -32,7 +32,7 @@ class ClientUser extends Structures.get('User') { * @type {Presence} */ get presence() { - return this.client.presences.clientPresence; + return this.client.presence; } edit(data) { @@ -97,7 +97,7 @@ class ClientUser extends Structures.get('User') { * .catch(console.error); */ setPresence(data) { - return this.client.presences.setClientPresence(data); + return this.client.presence.setClientPresence(data); } /** diff --git a/src/structures/User.js b/src/structures/User.js index b05ddc241..4098fd95a 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -105,7 +105,6 @@ class User extends Base { * @readonly */ get presence() { - if (this.client.presences.has(this.id)) return this.client.presences.get(this.id); for (const guild of this.client.guilds.values()) { if (guild.presences.has(this.id)) return guild.presences.get(this.id); } diff --git a/src/util/Structures.js b/src/util/Structures.js index c81235033..8b1edd8b5 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -73,6 +73,7 @@ const structures = { Message: require('../structures/Message'), MessageReaction: require('../structures/MessageReaction'), Presence: require('../structures/Presence').Presence, + ClientPresence: require('../structures/ClientPresence'), VoiceState: require('../structures/VoiceState'), Role: require('../structures/Role'), User: require('../structures/User'), From 7968442dbfadb0345cffdd9f57abd3600e735520 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 11 Aug 2018 11:27:34 +0100 Subject: [PATCH 0790/1359] VoiceChannel#members should be a Collection, not an Array --- src/structures/VoiceChannel.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index c8b431076..17d1ef502 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,6 +1,7 @@ const GuildChannel = require('./GuildChannel'); const { browser } = require('../util/Constants'); const Permissions = require('../util/Permissions'); +const Collection = require('../util/Collection'); const { Error } = require('../errors'); /** @@ -29,9 +30,9 @@ class VoiceChannel extends GuildChannel { * @name VoiceChannel#members */ get members() { - return this.guild.voiceStates + return new Collection(this.guild.voiceStates .filter(state => state.channelID === this.id && state.member) - .map(state => state.member); + .map(state => state.member)); } /** From 8ac664801c38e7fe59b2ea4a6c930a88ff1f707e Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 11 Aug 2018 11:34:24 +0100 Subject: [PATCH 0791/1359] GuildChannel#setParent should allow channel to be a Snowflake (#2644) --- 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 fcdbab224..287ee7a87 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -391,7 +391,7 @@ class GuildChannel extends Channel { */ setParent(channel, { lockPermissions = true, reason } = {}) { return this.edit({ - parentID: channel !== null ? channel.id ? channel.id : channel : null, + parentID: channel !== null ? channel.hasOwnProperty('id') ? channel.id : channel : null, lockPermissions, }, reason); } From e2ceea65baac67b6b9ad2ca24ba1aa38bb81585b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 11 Aug 2018 11:48:18 +0100 Subject: [PATCH 0792/1359] Fix default presence for Users --- src/structures/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/User.js b/src/structures/User.js index 4098fd95a..26a08a853 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -108,7 +108,7 @@ class User extends Base { for (const guild of this.client.guilds.values()) { if (guild.presences.has(this.id)) return guild.presences.get(this.id); } - return new Presence(this.client); + return new Presence(this.client, { user: { id: this.id } }); } /** From b7e61f21ca50b33d8cba3f74f30d5c39bb66fe54 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 11 Aug 2018 11:24:30 -0500 Subject: [PATCH 0793/1359] fix(PresenceStore): pass user id as extra (#2733) fix #2731 --- src/stores/PresenceStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index dc758d3c3..15fc9f124 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -12,7 +12,7 @@ class PresenceStore extends DataStore { add(data, cache) { const existing = this.get(data.user.id); - return existing ? existing.patch(data) : super.add(data, cache); + return existing ? existing.patch(data) : super.add(data, cache, { id: data.user.id }); } /** From c1183f353405444b11a071fb6b680c70f00a7f1f Mon Sep 17 00:00:00 2001 From: Jacz Date: Sun, 12 Aug 2018 11:16:34 +1000 Subject: [PATCH 0794/1359] Fixes VoiceChannel.members bug Closes #2736 --- src/structures/VoiceChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 17d1ef502..5e9d61218 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -32,7 +32,7 @@ class VoiceChannel extends GuildChannel { get members() { return new Collection(this.guild.voiceStates .filter(state => state.channelID === this.id && state.member) - .map(state => state.member)); + .map(state => [state.id, state.member])); } /** From f9da255b44e424d9debf9887d13da5d9947bc2b5 Mon Sep 17 00:00:00 2001 From: Jacz Date: Sun, 12 Aug 2018 14:43:37 +1000 Subject: [PATCH 0795/1359] More preformant way, forgot to commit this before whoops --- src/structures/VoiceChannel.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 5e9d61218..048342134 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -30,9 +30,13 @@ class VoiceChannel extends GuildChannel { * @name VoiceChannel#members */ get members() { - return new Collection(this.guild.voiceStates - .filter(state => state.channelID === this.id && state.member) - .map(state => [state.id, state.member])); + const coll = new Collection(); + for (const state of this.guild.voiceStates.values()) { + if (state.channelID === this.id && state.member) { + coll.set(state.id, state.member); + } + } + return coll; } /** From 180813d3e8157d54134f143e7b6b208c463a7dd7 Mon Sep 17 00:00:00 2001 From: Jacz Date: Sun, 12 Aug 2018 15:49:52 +1000 Subject: [PATCH 0796/1359] Consistency with Getters. --- src/structures/VoiceState.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index ebe104747..88e0ed196 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -58,7 +58,7 @@ class VoiceState extends Base { * @type {?GuildMember} */ get member() { - return this.guild.members.get(this.id); + return this.guild.members.get(this.id) || null; } /** @@ -66,7 +66,7 @@ class VoiceState extends Base { * @type {?VoiceChannel} */ get channel() { - return this.guild.channels.get(this.channelID); + return this.guild.channels.get(this.channelID) || null; } /** From 332e30d31ecb75cbaeca2e1b76083f4f2dd0d39f Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 13 Aug 2018 16:02:05 +0100 Subject: [PATCH 0797/1359] fix: GuildMemberStore now patches existing members (#1870) --- src/stores/GuildMemberStore.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 520f9d0d3..634179499 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -14,8 +14,12 @@ class GuildMemberStore extends DataStore { this.guild = guild; } - add(data, cache) { - return super.add(data, cache, { extras: [this.guild] }); + add(data, cache = true) { + const existing = this.get(data.user.id); + if (existing) return existing._patch(data); + const entry = new GuildMember(this.client, data, this.guild); + if (cache) this.set(data.user.id, entry); + return entry; } /** From d276850c522571555da03d89e41e761c988910bd Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 13 Aug 2018 16:21:11 +0100 Subject: [PATCH 0798/1359] fix: GuildMemberStore now works with custom structures again --- src/stores/GuildMemberStore.js | 6 +----- src/structures/Message.js | 4 +++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 634179499..44494be49 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -15,11 +15,7 @@ class GuildMemberStore extends DataStore { } add(data, cache = true) { - const existing = this.get(data.user.id); - if (existing) return existing._patch(data); - const entry = new GuildMember(this.client, data, this.guild); - if (cache) this.set(data.user.id, entry); - return entry; + return super.add(data, cache, { id: data.user.id, extras: [this.guild] }); } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index 7137b6266..8cc16ba23 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -164,7 +164,9 @@ class Message extends Base { */ this._edits = []; - if (data.member && this.guild && this.author) { + if (this.member) { + this.member._patch(data.member); + } else if (data.member && this.guild && this.author) { this.guild.members.add(Object.assign(data.member, { user: this.author })); } } From edf9c448fbbd6bd6ae7d5f9dcce75d96468adaa8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 13 Aug 2018 16:49:10 +0100 Subject: [PATCH 0799/1359] fix: message passing empty data to GuildMember#_patch --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 8cc16ba23..a443b234a 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -164,7 +164,7 @@ class Message extends Base { */ this._edits = []; - if (this.member) { + if (this.member && data.member) { this.member._patch(data.member); } else if (data.member && this.guild && this.author) { this.guild.members.add(Object.assign(data.member, { user: this.author })); From bfde1dd8f2d7785dd2974c5310fac45634637855 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 13 Aug 2018 17:35:21 +0100 Subject: [PATCH 0800/1359] fix: StreamDispatcher doesn't emit finish if ended while paused (#2648) --- src/client/voice/dispatcher/StreamDispatcher.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 4b6a5f860..748e4f3f8 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -121,8 +121,11 @@ class StreamDispatcher extends Writable { * Pauses playback */ pause() { + if (this.paused) return; + if (this.streams.opus) this.streams.opus.unpipe(this); + this._writeCallback(); this._setSpeaking(false); - if (!this.paused) this.pausedSince = Date.now(); + this.pausedSince = Date.now(); } /** @@ -142,6 +145,7 @@ class StreamDispatcher extends Writable { */ resume() { if (!this.pausedSince) return; + if (this.streams.opus) this.streams.opus.pipe(this); this._pausedTime += Date.now() - this.pausedSince; this.pausedSince = null; if (typeof this._writeCallback === 'function') this._writeCallback(); From 6852a15cee416d0c6c8558161006fbe76fcc8243 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 13 Aug 2018 17:54:11 +0100 Subject: [PATCH 0801/1359] voice: fix StreamDispatcher#pause trying to call null function --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 748e4f3f8..e79531b02 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -123,7 +123,7 @@ class StreamDispatcher extends Writable { pause() { if (this.paused) return; if (this.streams.opus) this.streams.opus.unpipe(this); - this._writeCallback(); + if (this._writeCallback) this._writeCallback(); this._setSpeaking(false); this.pausedSince = Date.now(); } From bb0700ade38300a7c87ce800d26c3a523b2fe1a9 Mon Sep 17 00:00:00 2001 From: Isabella Date: Mon, 13 Aug 2018 15:59:35 -0500 Subject: [PATCH 0802/1359] fix: Util#cleanContent parsing of GuildMember --- src/util/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index a214695a0..1098864f9 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -399,7 +399,7 @@ class Util { const member = message.channel.guild.members.get(id); if (member) { - return member.displayName; + return `@${member.displayName}`; } else { const user = message.client.users.get(id); return user ? `@${user.username}` : input; From afcac9196d5d450442410ae7d7776cc8fa9c17f4 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 13 Aug 2018 21:46:14 -0400 Subject: [PATCH 0803/1359] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 16 +++++++++------- .github/ISSUE_TEMPLATE/feature_request.md | 7 ++++--- .../question---general-support-request.md | 13 +++++++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/question---general-support-request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 266c71ea3..c497cb4fa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,13 +1,14 @@ --- name: Bug report -about: Report incorrect or unexpected behavior of discord.js +about: Report incorrect or unexpected behaviour of discord.js --- **Please describe the problem you are having in as much detail as possible:** @@ -16,21 +17,22 @@ This issue tracker is only for bug reports and enhancement suggestions. You won' **Include a reproducible code sample here, if possible:** ```js +// Place your code here + ``` **Further details:** - - discord.js version: -- node.js version: +- Node.js version: - Operating system: - Priority this issue should have – please be realistic and elaborate if possible: - [ ] I found this issue while running code on a __user account__ - [ ] I have also tested the issue on latest master, commit hash: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b9f212020..ee9d888af 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,19 +1,20 @@ --- name: Feature request -about: Request a new feature that discord.js is missing +about: Request a feature for the core discord.js library --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -**Describe the solution you'd like** +**Describe the ideal solution** A clear and concise description of what you want to happen. **Describe alternatives you've considered** diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md new file mode 100644 index 000000000..8ec717c90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question---general-support-request.md @@ -0,0 +1,13 @@ +--- +name: Question / General support request +about: Ask for help in Discord instead - https://discord.gg/bRCvFy9 + +--- + +Seriously, we only use this issue tracker for bugs in the library itself and feature requests for it. +We don't typically answer questions or help with support issues here. +Instead, please ask in one of the support channels in our Discord server: + +https://discord.gg/bRCvFy9 + +Any issues that don't directly involve a bug in the library or a feature request will likely be closed and redirected to the Discord server. From 1ff56eb09ddb74826bf474cd6b1fe405301632e7 Mon Sep 17 00:00:00 2001 From: zajrik Date: Mon, 13 Aug 2018 20:51:41 -0500 Subject: [PATCH 0804/1359] Merge typings into master --- .gitmodules | 3 - .npmignore | 2 + package.json | 4 + tsconfig.json | 13 + tslint.json | 62 ++ typings | 1 - typings/discord.js-test.ts | 69 ++ typings/index.d.ts | 2151 ++++++++++++++++++++++++++++++++++++ 8 files changed, 2301 insertions(+), 4 deletions(-) delete mode 100644 .gitmodules create mode 100644 tsconfig.json create mode 100644 tslint.json delete mode 160000 typings create mode 100644 typings/discord.js-test.ts create mode 100644 typings/index.d.ts diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 44fff6d5f..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "typings"] - path = typings - url = https://github.com/discordjs/discord.js-typings diff --git a/.npmignore b/.npmignore index 0ad23d8df..9d6674214 100644 --- a/.npmignore +++ b/.npmignore @@ -22,3 +22,5 @@ docs/ webpack.config.js .github/ test/ +tsconfig.json +tslint.json diff --git a/package.json b/package.json index 8287ade82..0caadd512 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "docs:test": "docgen --source src --custom docs/index.yml", "lint": "eslint src *.js", "lint:fix": "eslint --fix src", + "lint:typings": "tslint index.d.ts discord.js-test.ts", "build:browser": "webpack", "prepublishOnly": "npm run test && NODE_ENV=production npm run build:browser" }, @@ -54,6 +55,9 @@ "discord.js-docgen": "discordjs/docgen", "eslint": "^5.0.1", "json-filter-loader": "^1.0.0", + "tslint": "^3.15.1", + "tslint-config-typings": "^0.2.4", + "typescript": "^3.0.1", "uglifyjs-webpack-plugin": "^1.1.8", "webpack": "^4.5.0", "webpack-cli": "^3.0.1" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..63656868b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": true, + "strictNullChecks": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "typings/index.d.ts" + ] +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 000000000..6cc6f6b26 --- /dev/null +++ b/tslint.json @@ -0,0 +1,62 @@ +{ + "extends": [ + "tslint-config-typings" + ], + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "indent": [ + true, + "tabs" + ], + "no-duplicate-variable": true, + "no-unused-variable": [false], + "no-eval": true, + "no-internal-module": true, + "no-trailing-whitespace": true, + "no-unsafe-finally": true, + "no-var-keyword": true, + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": [ + true, + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file diff --git a/typings b/typings deleted file mode 160000 index db2a6e498..000000000 --- a/typings +++ /dev/null @@ -1 +0,0 @@ -Subproject commit db2a6e498bc625087cdd873a4af637539c4d4cfd diff --git a/typings/discord.js-test.ts b/typings/discord.js-test.ts new file mode 100644 index 000000000..6792dba87 --- /dev/null +++ b/typings/discord.js-test.ts @@ -0,0 +1,69 @@ +/// + +import { Collector, Message, CollectorFilter, Client, CollectorHandler, MessageReaction, Collection, User, ReactionCollectorOptions, Snowflake } from 'discord.js'; + +const client = new Client({ + disableEveryone: false, + disabledEvents: ['GUILD_MEMBER_ADD'] +}); + +client.on('message', (message) => { + if (message.content === 'hello') { + message.channel.sendMessage('o/'); + } + + const collector: ReactionCollector = new ReactionCollector(message, + (reaction: MessageReaction) => reaction.emoji.toString() === '👌', + { time: 30e3 }); + collector.on('end', collected => console.log(collected)); +}); + +client.login('dsfsd754.4fds4f68d4f6sd46f4s.7878easfdsgdfFDSIJIO'); + +export class TestCollector extends Collector { + public filter: CollectorFilter; + public constructor(client: Client, filter: CollectorFilter, ) { + super(client, filter); + } + + public handle(message: Message): CollectorHandler { + return { key: message.id, value: message }; + } + + public cleanup(): void {} + public postCheck(): null { return null; } +} + +class ReactionCollector extends Collector { + public message: Message; + public users: Collection; + public total: number; + public options: ReactionCollectorOptions; + public constructor(message: Message, filter: CollectorFilter, options?: ReactionCollectorOptions) { + super(message.client, filter, options || {}); + this.message = message; + this.users = new Collection(); + this.total = 0; + this.client.on('messageReactionAdd', this.listener); + } + + public handle(reaction: MessageReaction): CollectorHandler { + if (reaction.message.id !== this.message.id) { return null; } + return { + key: reaction.emoji.id || reaction.emoji.name, + value: reaction + }; + } + + public postCheck(reaction: MessageReaction, user: User): string { + this.users.set(user.id, user); + if (this.options.max && ++this.total >= this.options.max) { return 'limit'; } + if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) { return 'emojiLimit'; } + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) { return 'userLimit'; } + return null; + } + + public cleanup(): void { + this.client.removeListener('messageReactionAdd', this.listener); + } +} diff --git a/typings/index.d.ts b/typings/index.d.ts new file mode 100644 index 000000000..07b674f04 --- /dev/null +++ b/typings/index.d.ts @@ -0,0 +1,2151 @@ +// Type definitions for discord.js 12.0.0 +// Project: https://github.com/hydrabolt/discord.js +// Definitions by: +// acdenisSK (https://github.com/acdenisSK) +// Zack Campbell (https://github.com/zajrik) +// iCrawl (https://github.com/iCrawl) +// License: MIT + +declare module 'discord.js' { + import { EventEmitter } from 'events'; + import { Stream, Readable, Writable } from 'stream'; + import { ChildProcess } from 'child_process'; + + export const version: string; + +//#region Classes + + export class Activity { + constructor(presence: Presence, data: object); + public applicationID: Snowflake; + public assets: RichPresenceAssets; + public details: string; + public name: string; + public party: { + id: string; + size: [number, number]; + }; + public state: string; + public timestamps: { + start: Date; + end: Date; + }; + public type: ActivityType; + public url: string; + public equals(activity: Activity): boolean; + } + + export class Base { + constructor (client: Client); + public readonly client: Client; + public toJSON(...props: { [key: string]: boolean | string }[]): object; + public valueOf(): string; + } + + export class BaseClient extends EventEmitter { + constructor(options?: ClientOptions); + private _intervals: Set; + private _timeouts: Set; + private readonly api: object; + private rest: object; + + public options: ClientOptions; + public clearInterval(interval: NodeJS.Timer): void; + public clearTimeout(timeout: NodeJS.Timer): void; + public destroy(): void; + public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public toJSON(...props: { [key: string]: boolean | string }[]): object; + } + + class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { + public broadcast: VoiceBroadcast; + } + + export class CategoryChannel extends GuildChannel { + public readonly children: Collection; + } + + export class Channel extends Base { + constructor(client: Client, data: object); + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public deleted: boolean; + public id: Snowflake; + public type: 'dm' | 'group' | 'text' | 'voice' | 'category' | 'unknown'; + public delete(reason?: string): Promise; + public toString(): string; + } + + + export class Client extends BaseClient { + constructor(options?: ClientOptions); + private readonly _pingTimestamp: number; + private actions: object; + private manager: ClientManager; + private voice: object; + private ws: object; + private _eval(script: string): any; + private _pong(startTime: number): void; + private _validateOptions(options?: ClientOptions): void; + + public broadcasts: VoiceBroadcast[]; + public channels: ChannelStore; + public readonly emojis: GuildEmojiStore; + public guilds: GuildStore; + public readonly ping: number; + public pings: number[]; + public presences: ClientPresenceStore; + public readyAt: Date; + public readonly readyTimestamp: number; + public shard: ShardClientUtil; + public readonly status: Status; + public token: string; + public readonly uptime: number; + public user: ClientUser; + public users: UserStore; + public readonly voiceConnections: Collection; + public createVoiceBroadcast(): VoiceBroadcast; + public destroy(): Promise; + public fetchApplication(id?: Snowflake): Promise; + public fetchInvite(invite: InviteResolvable): Promise; + public fetchVoiceRegions(): Promise>; + public fetchWebhook(id: Snowflake, token?: string): Promise; + public generateInvite(permissions?: number | PermissionResolvable[]): Promise; + public login(token?: string): Promise; + public sweepMessages(lifetime?: number): number; + public syncGuilds(guilds?: Guild[] | Collection): void; + public toJSON(): object; + + public on(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; + public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; + public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public on(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; + public on(event: 'clientUserSettingsUpdate', listener: (clientUserSettings: ClientUserSettings) => void): this; + public on(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; + public on(event: 'debug' | 'warn', listener: (info: string) => void): this; + public on(event: 'disconnect', listener: (event: any) => void): this; + public on(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; + public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; + public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; + public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; + public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public on(event: 'guildMemberUpdate' | 'presenceUpdate' | 'voiceStateUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; + public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; + public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; + public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; + public on(event: 'ready' | 'reconnecting', listener: () => void): this; + public on(event: 'resumed', listener: (replayed: number) => void): this; + public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; + public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; + public on(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; + public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; + public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; + public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public once(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; + public once(event: 'clientUserSettingsUpdate', listener: (clientUserSettings: ClientUserSettings) => void): this; + public once(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; + public once(event: 'debug' | 'warn', listener: (info: string) => void): this; + public once(event: 'disconnect', listener: (event: any) => void): this; + public once(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; + public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; + public once(event: 'error', listener: (error: Error) => void): this; + public once(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; + public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; + public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; + public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public once(event: 'guildMemberUpdate' | 'presenceUpdate' | 'voiceStateUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; + public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; + public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; + public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; + public once(event: 'ready' | 'reconnecting', listener: () => void): this; + public once(event: 'resumed', listener: (replayed: number) => void): this; + public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; + public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; + public once(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; + public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public once(event: string, listener: Function): this; + } + + export class ClientApplication extends Base { + constructor(client: Client, data: object); + public bot: object; + public botPublic: boolean; + public botRequireCodeGrant: boolean; + public cover: string; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public description: string; + public icon: string; + public id: Snowflake; + public name: string; + public owner: User; + public redirectURIs: string[]; + public rpcApplicationState: boolean; + public rpcOrigins: string[]; + public secret: string; + public coverImage(options?: AvatarOptions): string; + public createAsset(name: string, data: Base64Resolvable, type: 'big' | 'small' | 'Big' | 'Small'): Promise; + public fetchAssets(): Promise; + public iconURL(options?: AvatarOptions): string; + public resetSecret(): Promise; + public resetToken(): Promise; + public toJSON(): object; + public toString(): string; + } + + class ClientManager { + constructor(client: Client); + public client: Client; + public heartbeatInterval: number; + public readonly status: number; + public connectToWebSocket(token: string, resolve: Function, reject: Function): void; + } + + export class ClientUser extends User { + public blocked: Collection; + public email: string; + public friends: Collection; + public guildSettings: Collection; + public mfaEnabled: boolean; + public mobile: boolean; + public notes: Collection; + public premium: boolean; + public settings: ClientUserSettings; + public verified: boolean; + public createGroupDM(recipients: GroupDMRecipientOptions[]): Promise; + public fetchMentions(options?: { limit?: number; roles?: boolean, everyone?: boolean; guild?: Guild | Snowflake }): Promise; + public setActivity(name: string, options?: { url?: string, type?: ActivityType | number }): Promise; + public setAFK(afk: boolean): Promise; + public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; + public setEmail(email: string, password: string): Promise; + public setPassword(newPassword: string, options: { oldPassword?: string, mfaCode?: string }): Promise; + public setPresence(data: PresenceData): Promise; + public setStatus(status: PresenceStatus): Promise; + public setUsername(username: string, password?: string): Promise; + public toJSON(): object; + } + + export class ClientUserChannelOverride { + constructor(data: object); + private patch(data: object): void; + + public messageNotifications: GuildChannelMessageNotifications; + public muted: boolean; + } + + export class ClientUserGuildSettings { + constructor(client: Client, data: object); + private patch(data: object): void; + private update(name: string, value: any): Promise; + + public channelOverrides: Collection; + public readonly client: Client; + public guildID: Snowflake; + public messageNotifications: GuildChannelMessageNotifications; + public mobilePush: boolean; + public muted: boolean; + public suppressEveryone: boolean; + } + + export class ClientUserSettings { + constructor(user: User, data: object); + private update(name: string, value: any): Promise; + private patch(data: object): void; + + public convertEmoticons: boolean; + public defaultGuildsRestricted: boolean; + public detectPlatformAccounts: boolean; + public developerMode: boolean; + public enableTTSCommand: boolean; + public explicitContentFilter: 'DISABLED' | 'NON_FRIENDS' | 'FRIENDS_AND_NON_FRIENDS'; + public friendsSources: { all: boolean, mutualGuilds: boolean, mutualFriends: boolean }; + public guildsPositions: Snowflake[]; + public inlineAttachmentMedia: boolean; + public inlineEmbedMedia: boolean; + public locale: string; + public messageDisplayCompact: boolean; + public renderReactions: boolean; + public restrictedGuilds: Snowflake[]; + public showCurrentGame: boolean; + public status: PresenceStatus; + public theme: string; + public addRestrictedGuild(guild: Guild): Promise; + public removeRestrictedGuild(guild: Guild): Promise; + public setGuildPosition(guild: Guild, position: number, relative?: boolean): Promise; + } + + export class Collection extends Map { + private _array: V[]; + private _keyArray: K[]; + + public array(): V[]; + public clone(): Collection; + public concat(...collections: Collection[]): Collection; + public each(fn: (value: V, key: K, collection: Collection) => void, thisArg?: any): void; + public equals(collection: Collection): boolean; + public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; + public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; + public find(fn: (value: V, key: K, collection: Collection) => boolean): V; + public findKey(fn: (value: V, key: K, collection: Collection) => boolean): K; + public first(): V | undefined; + public first(count: number): V[]; + public firstKey(): K | undefined; + public firstKey(count: number): K[]; + public keyArray(): K[]; + public last(): V | undefined; + public last(count: number): V[]; + public lastKey(): K | undefined; + public lastKey(count: number): K[]; + public map(fn: (value: V, key: K, collection: Collection) => T, thisArg?: any): T[]; + public partition(fn: (value: V, key: K, collection: Collection) => boolean): [Collection, Collection] + public random(): V | undefined; + public random(count: number): V[]; + public randomKey(): K | undefined; + public randomKey(count: number): K[]; + public reduce(fn: (accumulator: any, value: V, key: K, collection: Collection) => T, initialValue?: any): T; + public some(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; + public sort(compareFunction?: (a: V, b: V, c?: K, d?: K) => number): Collection; + public sweep(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): number; + public tap(fn: (collection: Collection) => void, thisArg?: any): Collection; + public toJSON(): object; + } + + export abstract class Collector extends EventEmitter { + constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); + private _timeout: NodeJS.Timer; + + public readonly client: Client; + public collected: Collection; + public ended: boolean; + public filter: CollectorFilter; + public readonly next: Promise; + public options: CollectorOptions; + public checkEnd(): void; + public handleCollect(...args: any[]): void; + public handleDispose(...args: any[]): void; + public stop(reason?: string): void; + public toJSON(): object; + + protected listener: Function; + public abstract collect(...args: any[]): K; + public abstract dispose(...args: any[]): K; + public abstract endReason(): void; + + public on(event: 'collect', listener: (...args: any[]) => void): this; + public on(event: 'dispose', listener: (...args: any[]) => void): this; + public on(event: 'end', listener: (collected: Collection, reason: string) => void): this; + + public once(event: 'collect', listener: (...args: any[]) => void): this; + public once(event: 'dispose', listener: (...args: any[]) => void): this; + public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; + } + + export class DataResolver { + public static resolveBase64(data: Base64Resolvable): string; + public static resolveFile(resource: BufferResolvable | Stream): Promise; + public static resolveImage(resource: BufferResolvable | Base64Resolvable): Promise; + public static resolveInviteCode(data: InviteResolvable): string; + } + + export class DiscordAPIError extends Error { + constructor(path: string, error: object); + private static flattenErrors(obj: object, key: string): string[]; + + public code: number; + public method: string; + public path: string; + } + + export class DMChannel extends TextBasedChannel(Channel) { + constructor(client: Client, data: object); + public messages: MessageStore; + public recipient: User; + } + + export class Emoji extends Base { + constructor(client: Client, emoji: object); + public animated: boolean; + public readonly deletable: boolean; + public id: Snowflake; + public name: string; + public readonly identifier: string; + public readonly url: string; + public toJSON(): object; + public toString(): string; + } + + export class GroupDMChannel extends TextBasedChannel(Channel) { + constructor(client: Client, data: object); + public applicationID: Snowflake; + public icon: string; + public managed: boolean; + public messages: MessageStore; + public name: string; + public nicks: Collection; + public readonly owner: User; + public ownerID: Snowflake; + public recipients: Collection; + public addUser(options: { user: UserResolvable, accessToken?: string, nick?: string }): Promise + public edit (data: { icon?: string, name?: string }): Promise; + public equals(channel: GroupDMChannel): boolean; + public iconURL(options?: AvatarOptions): string; + public removeUser(user: UserResolvable): Promise; + public setIcon(icon: Base64Resolvable | BufferResolvable): Promise; + public setName(name: string): Promise; + } + + export class Guild extends Base { + constructor(client: Client, data: object); + private _sortedRoles(): Collection; + private _sortedChannels(channel: Channel): Collection; + private _memberSpeakUpdate(user: Snowflake, speaking: boolean): void; + + protected setup(data: any): void; + + public readonly afkChannel: VoiceChannel; + public afkChannelID: Snowflake; + public afkTimeout: number; + public applicationID: Snowflake; + public available: boolean; + public channels: GuildChannelStore; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public defaultMessageNotifications: DefaultMessageNotifications | number; + public deleted: boolean; + public embedEnabled: boolean; + public emojis: GuildEmojiStore; + public explicitContentFilter: number; + public features: GuildFeatures[]; + public icon: string; + public id: Snowflake; + public readonly joinedAt: Date; + public joinedTimestamp: number; + public large: boolean; + public readonly me: GuildMember; + public memberCount: number; + public members: GuildMemberStore; + public readonly messageNotifications: MessageNotifications; + public mfaLevel: number; + public readonly mobilePush: boolean; + public readonly muted: boolean; + public name: string; + public readonly nameAcronym: string; + public readonly owner: GuildMember; + public ownerID: Snowflake; + public readonly position: number; + public presences: PresenceStore; + public region: string; + public roles: RoleStore; + public splash: string; + public readonly suppressEveryone: boolean; + public readonly systemChannel: TextChannel; + public systemChannelID: Snowflake; + public verificationLevel: number; + public readonly verified: boolean; + public readonly voiceConnection: VoiceConnection; + public acknowledge(): Promise; + public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; + public allowDMs(allow: boolean): Promise; + public delete(): Promise; + public edit(data: GuildEditData, reason?: string): Promise; + public equals(guild: Guild): boolean; + public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; + public fetchBans(): Promise>; + public fetchInvites(): Promise>; + public fetchVoiceRegions(): Promise>; + public fetchWebhooks(): Promise>; + public iconURL(options?: AvatarOptions): string; + public leave(): Promise; + public member(user: UserResolvable): GuildMember; + public search(options?: MessageSearchOptions): Promise; + public setAFKChannel(afkChannel: ChannelResolvable, reason?: string): Promise; + public setAFKTimeout(afkTimeout: number, reason?: string): Promise; + public setChannelPositions(channelPositions: ChannelPosition[]): Promise; + public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications | number, reason?: string): Promise; + public setExplicitContentFilter(explicitContentFilter: number, reason?: string): Promise; + public setIcon(icon: Base64Resolvable, reason?: string): Promise; + public setName(name: string, reason?: string): Promise; + public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; + public setPosition(position: number, relative?: boolean): Promise; + public setRegion(region: string, reason?: string): Promise; + public setSplash(splash: Base64Resolvable, reason?: string): Promise; + public setSystemChannel(systemChannel: ChannelResolvable, reason?: string): Promise; + public setVerificationLevel(verificationLevel: number, reason?: string): Promise; + public splashURL(options?: AvatarOptions): string; + public sync(): void; + public toJSON(): object; + public toString(): string; + } + + export class GuildAuditLogs { + constructor(guild: Guild, data: object); + private webhooks: Collection; + + public entries: Collection; + + public static Actions: GuildAuditLogsActions; + public static Targets: GuildAuditLogsTargets; + public static Entry: typeof GuildAuditLogsEntry; + public static actionType(action: number): GuildAuditLogsActionType; + public static build(...args: any[]): Promise; + public static targetType(target: number): GuildAuditLogsTarget; + public toJSON(): object; + } + + class GuildAuditLogsEntry { + constructor(guild: Guild, data: object); + public action: GuildAuditLogsAction; + public actionType: GuildAuditLogsActionType; + public changes: AuditLogChange[]; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public executor: User; + public extra: object | Role | GuildMember; + public id: Snowflake; + public reason: string; + public target: Guild | User | Role | GuildEmoji | Invite | Webhook; + public targetType: GuildAuditLogsTarget; + public toJSON(): object; + } + + export class GuildChannel extends Channel { + constructor(guild: Guild, data: object); + private memberPermissions(member: GuildMember): Permissions; + private rolePermissions(role: Role): Permissions; + + public readonly calculatedPosition: number; + public readonly deletable: boolean; + public guild: Guild; + public readonly messageNotifications: GuildChannelMessageNotifications; + public readonly manageable: boolean; + public readonly muted: boolean; + public name: string; + public readonly parent: CategoryChannel; + public parentID: Snowflake; + public permissionOverwrites: Collection; + public readonly permissionsLocked: boolean; + public readonly position: number; + public rawPosition: number; + public clone(options?: GuildChannelCloneOptions): Promise; + public createInvite(options?: InviteOptions): Promise; + public edit(data: ChannelData, reason?: string): Promise; + public equals(channel: GuildChannel): boolean; + public fetchInvites(): Promise>; + public lockPermissions(): Promise; + public overwritePermissions( + options: Array> | Collection>, + reason?: string + ): Promise; + public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Permissions; + public setName(name: string, reason?: string): Promise; + public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; + public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; + public setTopic(topic: string, reason?: string): Promise; + public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: Partial, reason?: string): Promise; + } + + export class GuildEmoji extends Emoji { + constructor(client: Client, data: object, guild: Guild); + private _roles: string[]; + + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public deleted: boolean; + public guild: Guild; + public managed: boolean; + public requiresColons: boolean; + public roles: GuildEmojiRoleStore; + public delete(reason?: string): Promise; + public edit(data: GuildEmojiEditData, reason?: string): Promise; + public equals(other: GuildEmoji | object): boolean; + public setName(name: string, reason?: string): Promise; + } + + export class GuildMember extends PartialTextBasedChannel(Base) { + constructor(client: Client, data: object, guild: Guild); + public readonly bannable: boolean; + public readonly deaf: boolean; + public deleted: boolean; + public readonly displayColor: number; + public readonly displayHexColor: string; + public readonly displayName: string; + public guild: Guild; + public readonly id: Snowflake; + public readonly joinedAt: Date; + public joinedTimestamp: number; + public readonly kickable: boolean; + public readonly manageable: boolean; + public readonly mute: boolean; + public nickname: string; + public readonly permissions: Permissions; + public readonly presence: Presence; + public roles: GuildMemberRoleStore; + public readonly selfDeaf: boolean; + public readonly selfMute: boolean; + public readonly serverDeaf: boolean; + public readonly serverMute: boolean; + public readonly speaking: boolean; + public user: User; + public readonly voiceChannel: VoiceChannel; + public readonly voiceChannelID: Snowflake; + public readonly voiceSessionID: string; + public ban(options?: BanOptions): Promise; + public createDM(): Promise; + public deleteDM(): Promise; + public edit(data: GuildMemberEditData, reason?: string): Promise; + public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean; + public kick(reason?: string): Promise; + public missingPermissions(permissions: PermissionResolvable, explicit?: boolean): PermissionString[]; + public permissionsIn(channel: ChannelResolvable): Permissions; + public setDeaf(deaf: boolean, reason?: string): Promise; + public setMute(mute: boolean, reason?: string): Promise; + public setNickname(nickname: string, reason?: string): Promise; + public setVoiceChannel(voiceChannel: ChannelResolvable): Promise; + public toJSON(): object; + public toString(): string; + } + + export class Invite extends Base { + constructor(client: Client, data: object); + public channel: GuildChannel | GroupDMChannel; + public code: string; + public readonly createdAt: Date; + public createdTimestamp: number; + public readonly expiresAt: Date; + public readonly expiresTimestamp: number; + public guild: Guild; + public inviter: User; + public maxAge: number; + public maxUses: number; + public memberCount: number; + public presenceCount: number; + public temporary: boolean; + public textChannelCount: number; + public readonly url: string; + public uses: number; + public voiceChannelCount: number; + public delete(reason?: string): Promise; + public toJSON(): object; + public toString(): string; + } + + export class Message extends Base { + constructor(client: Client, data: object, channel: TextChannel | DMChannel | GroupDMChannel); + private _edits: Message[]; + private patch(data: object): void; + + public activity: GroupActivity; + public application: ClientApplication; + public attachments: Collection; + public author: User; + public channel: TextChannel | DMChannel | GroupDMChannel; + public readonly cleanContent: string; + public content: string; + public readonly createdAt: Date; + public createdTimestamp: number; + public readonly deletable: boolean; + public deleted: boolean; + public readonly editable: boolean; + public readonly editedAt: Date; + public editedTimestamp: number; + public readonly edits: Message[]; + public embeds: MessageEmbed[]; + public readonly guild: Guild; + public hit: boolean; + public id: Snowflake; + public member: GuildMember; + public mentions: MessageMentions; + public nonce: string; + public readonly pinnable: boolean; + public pinned: boolean; + public reactions: ReactionStore; + public system: boolean; + public tts: boolean; + public type: MessageType; + public webhookID: Snowflake; + public acknowledge(): Promise; + public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; + public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; + public delete(options?: { timeout?: number, reason?: string }): Promise; + public edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise; + public equals(message: Message, rawData: object): boolean; + public fetchWebhook(): Promise; + public pin(): Promise; + public react(emoji: EmojiIdentifierResolvable): Promise; + public reply(content?: StringResolvable, options?: MessageOptions): Promise; + public reply(options?: MessageOptions): Promise; + public toJSON(): object; + public toString(): string; + public unpin(): Promise; + } + + export class MessageAttachment { + constructor(file: BufferResolvable | Stream, name?: string); + private _attach(file: BufferResolvable | Stream, name: string): void; + + public readonly attachment: BufferResolvable | Stream; + public height: number; + public id: Snowflake; + public readonly name: string; + public proxyURL: string; + public url: string; + public width: number; + public setAttachment(file: BufferResolvable | Stream, name: string): this; + public setFile(attachment: BufferResolvable | Stream): this; + public setName(name: string): this; + public toJSON(): object; + } + + export class MessageCollector extends Collector { + constructor(channel: TextChannel | DMChannel | GroupDMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); + public channel: Channel; + public options: MessageCollectorOptions; + public received: number; + + public collect(message: Message): Snowflake; + public dispose(message: Message): Snowflake; + public endReason(): string; + } + + export class MessageEmbed { + constructor(data?: MessageEmbed | MessageEmbedOptions); + private _apiTransform(): MessageEmbedOptions; + + public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string }; + public color: number; + public readonly createdAt: Date; + public description: string; + public fields: { name: string; value: string; inline?: boolean; }[]; + public files: (MessageAttachment | string | FileOptions)[]; + public footer: { text?: string; iconURL?: string; proxyIconURL?: string }; + public readonly hexColor: string; + public image: { url: string; proxyURL?: string; height?: number; width?: number; }; + public provider: { name: string; url: string; }; + public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; }; + public timestamp: number; + public title: string; + public type: string; + public url: string; + public readonly video: { url?: string; height?: number; width?: number }; + public addBlankField(inline?: boolean): this; + public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; + public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; + public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; + public setColor(color: ColorResolvable): this; + public setDescription(description: StringResolvable): this; + public setFooter(text: StringResolvable, iconURL?: string): this; + public setImage(url: string): this; + public setThumbnail(url: string): this; + public setTimestamp(timestamp?: Date): this; + public setTitle(title: StringResolvable): this; + public setURL(url: string): this; + public toJSON(): object; + } + + export class MessageMentions { + constructor(message: Message, users: any[], roles: any[], everyone: boolean); + private _channels: Collection; + private readonly _content: Message; + private _members: Collection; + + public readonly channels: Collection; + public readonly client: Client; + public everyone: boolean; + public readonly guild: Guild; + public has(data: User | GuildMember | Role | GuildChannel, options?: { + ignoreDirect?: boolean; + ignoreRoles?: boolean; + ignoreEveryone?: boolean; + }): boolean; + public readonly members: Collection; + public roles: Collection; + public users: Collection; + public toJSON(): object; + + public static CHANNELS_PATTERN: RegExp; + public static EVERYONE_PATTERN: RegExp; + public static ROLES_PATTERN: RegExp; + public static USERS_PATTERN: RegExp; + } + + export class MessageReaction { + constructor(client: Client, data: object, message: Message); + private _emoji: GuildEmoji | ReactionEmoji; + + public count: number; + public readonly emoji: GuildEmoji | ReactionEmoji; + public me: boolean; + public message: Message; + public users: ReactionUserStore; + public toJSON(): object; + } + + export class PermissionOverwrites { + constructor(guildChannel: GuildChannel, data: object); + public allowed: Permissions; + public readonly channel: GuildChannel; + public denied: Permissions; + public id: Snowflake; + public type: OverwriteType; + public delete(reason?: string): Promise; + public toJSON(): object; + } + + export class Permissions { + constructor(permissions: PermissionResolvable); + + public bitfield: number; + public add(...permissions: PermissionResolvable[]): this; + public freeze(): this; + public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; + public missing(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionString[]; + public remove(...permissions: PermissionResolvable[]): this; + public serialize(checkAdmin?: boolean): PermissionObject; + public toArray(checkAdmin?: boolean): PermissionString[]; + public toJSON(): object; + public valueOf(): number; + public [Symbol.iterator](): IterableIterator; + + public static ALL: number; + public static DEFAULT: number; + public static FLAGS: PermissionFlags; + public static resolve(permission: PermissionResolvable): number; + } + + export class Presence { + constructor(client: Client, data: object); + public activity: Activity; + public status: 'online' | 'offline' | 'idle' | 'dnd'; + public equals(presence: Presence): boolean; + } + + export class ReactionCollector extends Collector { + constructor(message: Message, filter: CollectorFilter, options?: ReactionCollectorOptions); + public message: Message; + public options: ReactionCollectorOptions; + public total: number; + public users: Collection; + + public static key(reaction: MessageReaction): Snowflake | string; + + public collect(reaction: MessageReaction): Snowflake | string; + public dispose(reaction: MessageReaction, user: User): Snowflake | string; + public empty(): void; + public endReason(): string; + + public on(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; + public on(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; + public on(event: 'end', listener: (collected: Collection, reason: string) => void): this; + public on(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; + public once(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; + public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; + public once(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; + public once(event: string, listener: Function): this; +} + + export class ReactionEmoji extends Emoji { + constructor(reaction: MessageReaction, emoji: object); + public reaction: MessageReaction; + public toJSON(): object; + } + + export class RichPresenceAssets { + constructor(activity: Activity, assets: object); + public largeImage: Snowflake; + public largeText: string; + public smallImage: Snowflake; + public smallText: string; + public largeImageURL(options: AvatarOptions): string; + public smallImageURL(options: AvatarOptions): string; + } + + export class Role extends Base { + constructor(client: Client, data: object, guild: Guild); + public color: number; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public deleted: boolean; + public readonly editable: boolean; + public guild: Guild; + public readonly hexColor: string; + public hoist: boolean; + public id: Snowflake; + public managed: boolean; + public readonly members: Collection; + public mentionable: boolean; + public name: string; + public permissions: Permissions; + public readonly position: number; + public rawPosition: number; + public comparePositionTo(role: Role): number; + public delete(reason?: string): Promise; + public edit(data: RoleData, reason?: string): Promise; + public equals(role: Role): boolean; + public permissionsIn(channel: ChannelResolvable): Permissions; + public setColor(color: ColorResolvable, reason?: string): Promise; + public setHoist(hoist: boolean, reason?: string): Promise; + public setMentionable(mentionable: boolean, reason?: string): Promise; + public setName(name: string, reason?: string): Promise; + public setPermissions(permissions: PermissionResolvable, reason?: string): Promise; + public setPosition(position: number, options?: { relative?: boolean; reason?: string }): Promise; + public toJSON(): object; + public toString(): string; + + public static comparePositions(role1: Role, role2: Role): number; + } + + export class Shard extends EventEmitter { + constructor(manager: ShardingManager, id: number, args?: string[]); + private _evals: Map>; + private _exitListener: Function; + private _fetches: Map>; + private _handleExit(respawn?: boolean): void; + private _handleMessage(message: any): void; + + public args: string[]; + public execArgv: string[]; + public env: object; + public id: number; + public manager: ShardingManager; + public process: ChildProcess; + public ready: boolean; + public eval(script: string): Promise; + public eval(fn: (client: Client) => T): Promise; + public fetchClientValue(prop: string): Promise; + public kill(): void; + public respawn(delay?: number, waitForReady?: boolean): Promise; + public send(message: any): Promise; + public spawn(waitForReady?: boolean): Promise; + + public on(event: 'death', listener: (child: ChildProcess) => void): this; + public on(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'message', listener: (message: any) => void): this; + public on(event: 'spawn', listener: (child: ChildProcess) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'death', listener: (child: ChildProcess) => void): this; + public once(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; + public once(event: 'error', listener: (error: Error) => void): this; + public once(event: 'message', listener: (message: any) => void): this; + public once(event: 'spawn', listener: (child: ChildProcess) => void): this; + public once(event: string, listener: Function): this; + } + + export class ShardClientUtil { + constructor(client: Client); + private _handleMessage(message: any): void; + private _respond(type: string, message: any): void; + + public readonly count: number; + public readonly id: number; + public broadcastEval(script: string): Promise; + public broadcastEval(fn: (client: Client) => T): Promise; + public fetchClientValues(prop: string): Promise; + public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise; + public send(message: any): Promise; + + public static singleton(client: Client): ShardClientUtil; + } + + export class ShardingManager extends EventEmitter { + constructor(file: string, options?: { + totalShards?: number | 'auto'; + respawn?: boolean; + shardArgs?: string[]; + token?: string; + execArgv?: string[]; + }); + + public file: string; + public respawn: boolean; + public shardArgs: string[]; + public shards: Collection; + public token: string; + public totalShards: number | 'auto'; + public broadcast(message: any): Promise; + public broadcastEval(script: string): Promise; + public createShard(id: number): Shard; + public fetchClientValues(prop: string): Promise; + public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise>; + public spawn(amount?: number | 'auto', delay?: number, waitForReady?: boolean): Promise>; + + public on(event: 'shardCreate', listener: (shard: Shard) => void): this; + + public once(event: 'shardCreate', listener: (shard: Shard) => void): this; + } + + export class SnowflakeUtil { + public static deconstruct(snowflake: Snowflake): DeconstructedSnowflake; + public static generate(): Snowflake; + } + + const VolumeMixin: (base: Constructable) => Constructable; + + class StreamDispatcher extends VolumeMixin(Writable) { + constructor(player: object, options?: StreamOptions, streams?: object); + public player: object; + public pausedSince: number; + public broadcast: VoiceBroadcast; + public readonly paused: boolean; + public readonly pausedTime: boolean; + public readonly streamTime: number; + public readonly totalStreamTime: number; + public readonly bitrateEditable: boolean; + + public setBitrate(value: number | 'auto'): boolean; + public setPLP(value: number): boolean; + public setFEC(enabled: boolean): boolean; + public pause(): void; + public resume(): void; + + public on(event: 'close', listener: () => void): this; + public on(event: 'debug', listener: (info: string) => void): this; + public on(event: 'drain', listener: () => void): this; + public on(event: 'end', listener: () => void): this; + public on(event: 'error', listener: (err: Error) => void): this; + public on(event: 'finish', listener: () => void): this; + public on(event: 'pipe', listener: (src: Readable) => void): this; + public on(event: 'start', listener: () => void): this; + public on(event: 'speaking', listener: (speaking: boolean) => void): this; + public on(event: 'unpipe', listener: (src: Readable) => void): this; + public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'close', listener: () => void): this; + public once(event: 'debug', listener: (info: string) => void): this; + public once(event: 'drain', listener: () => void): this; + public once(event: 'end', listener: () => void): this; + public once(event: 'error', listener: (err: Error) => void): this; + public once(event: 'finish', listener: () => void): this; + public once(event: 'pipe', listener: (src: Readable) => void): this; + public once(event: 'start', listener: () => void): this; + public once(event: 'speaking', listener: (speaking: boolean) => void): this; + public once(event: 'unpipe', listener: (src: Readable) => void): this; + public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + public once(event: string, listener: Function): this; + } + + export class Structures { + static get(structure: K): Extendable[K]; + static get(structure: string): Function; + static extend(structure: K, extender: (baseClass: Extendable[K]) => T): T; + static extend(structure: string, extender: (baseClass: typeof Function) => T): T; + } + + export class TextChannel extends TextBasedChannel(GuildChannel) { + constructor(guild: Guild, data: object); + public readonly members: Collection; + public messages: MessageStore; + public nsfw: boolean; + public topic: string; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; + public setNSFW(nsfw: boolean, reason?: string): Promise; + public fetchWebhooks(): Promise>; + } + + export class User extends PartialTextBasedChannel(Base) { + constructor(client: Client, data: object); + public avatar: string; + public bot: boolean; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public discriminator: string; + public readonly defaultAvatarURL: string; + public readonly dmChannel: DMChannel; + public id: Snowflake; + public locale: string; + public readonly note: string; + public readonly presence: Presence; + public readonly tag: string; + public username: string; + public avatarURL(options?: AvatarOptions): string; + public createDM(): Promise; + public deleteDM(): Promise; + public displayAvatarURL(options?: AvatarOptions): string; + public equals(user: User): boolean; + public fetchProfile(): Promise; + public setNote(note: string): Promise; + public toString(): string; + public typingDurationIn(channel: ChannelResolvable): number; + public typingIn(channel: ChannelResolvable): boolean; + public typingSinceIn(channel: ChannelResolvable): Date; + } + + export class UserConnection { + constructor(user: User, data: object); + public id: string; + public integrations: object[]; + public name: string; + public revoked: boolean; + public type: string; + public user: User; + public toJSON(): object; + } + + class UserProfile extends Base { + constructor(user: User, data: object); + private _flags: number; + + public connections: Collection; + public readonly flags: UserFlags[]; + public mutualGuilds: Collection; + public premium: boolean; + public premiumSince: Date; + public user: User; + public toJSON(): object; + } + + export class Util { + public static basename(path: string, ext?: string): string; + public static binaryToID(num: string): Snowflake; + public static cleanContent(str: string, message: Message): string; + public static cloneObject(obj: object): object; + public static convertToBuffer(ab: ArrayBuffer | string): Buffer; + public static delayFor(ms: number): Promise; + public static discordSort(collection: Collection): Collection + public static escapeMarkdown(text: string, onlyCodeBlock?: boolean, onlyInlineCode?: boolean): string; + public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise; + public static flatten(obj: object, ...props: { [key: string]: boolean | string }[]): object; + public static idToBinary(num: Snowflake): string; + public static makeError(obj: { name: string, message: string, stack: string }): Error; + public static makePlainError(err: Error): { name: string, message: string, stack: string }; + public static mergeDefault(def: object, given: object): object; + public static moveElementInArray(array: any[], element: any, newIndex: number, offset?: boolean): number; + public static parseEmoji(text: string): { animated: boolean; name: string; id: string; }; + public static resolveColor(color: ColorResolvable): number; + public static resolveString(data: StringResolvable): string; + public static setPosition( + item: T, + position: number, + relative: boolean, + sorted: Collection, + route: object, + reason?: string, + ): Promise<{ id: Snowflake; position: number }[]>; + public static splitMessage(text: string, options?: SplitOptions): string | string[]; + public static str2ab(str: string): ArrayBuffer; + } + + class VoiceBroadcast extends EventEmitter { + constructor(client: Client); + public client: Client; + public readonly dispatcher: BroadcastDispatcher; + public play(input: string | Readable, options?: StreamOptions): BroadcastDispatcher; + + public on(event: 'end', listener: () => void): this; + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public on(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public on(event: 'warn', listener: (warning: string | Error) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'end', listener: () => void): this; + public once(event: 'error', listener: (error: Error) => void): this; + public once(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public once(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public once(event: 'warn', listener: (warning: string | Error) => void): this; + public once(event: string, listener: Function): this; + } + + export class VoiceChannel extends GuildChannel { + constructor(guild: Guild, data: object); + public bitrate: number; + public readonly connection: VoiceConnection; + public readonly full: boolean; + public readonly joinable: boolean; + public readonly members: Collection; + public readonly speakable: boolean; + public userLimit: number; + public join(): Promise; + public leave(): void; + public setBitrate(bitrate: number, reason?: string): Promise; + public setUserLimit(userLimit: number, reason?: string): Promise; + } + + class VoiceConnection extends EventEmitter { + constructor(voiceManager: object, channel: VoiceChannel); + private authentication: object; + private sockets: object; + private ssrcMap: Map; + private _disconnect(): void; + private authenticate(): void; + private authenticateFailed(reason: string): void; + private checkAuthenticated(): void; + private cleanup(): void; + private connect(): void; + private onReady(data: object): void; + private onSessionDescription(mode: string, secret: string): void; + private onSpeaking(data: object): void; + private reconnect(token: string, endpoint: string): void; + private sendVoiceStateUpdate(options: object): void; + private setSessionID(sessionID: string): void; + private setSpeaking(value: boolean): void; + private setTokenAndEndpoint(token: string, endpoint: string): void; + private updateChannel(channel: VoiceChannel): void; + + public channel: VoiceChannel; + public readonly client: Client; + public readonly dispatcher: StreamDispatcher; + public player: object; + public receivers: VoiceReceiver[]; + public speaking: boolean; + public status: VoiceStatus; + public voiceManager: object; + public createReceiver(): VoiceReceiver; + public disconnect(): void; + public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; + + public on(event: 'authenticated', listener: () => void): this; + public on(event: 'closing', listener: () => void): this; + public on(event: 'debug', listener: (message: string) => void): this; + public on(event: 'disconnect', listener: (error: Error) => void): this; + public on(event: 'error', listener: (error: Error) => void): this; + public on(event: 'failed', listener: (error: Error) => void): this; + public on(event: 'newSession', listener: () => void): this; + public on(event: 'ready', listener: () => void): this; + public on(event: 'reconnecting', listener: () => void): this; + public on(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public on(event: 'warn', listener: (warning: string | Error) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'authenticated', listener: () => void): this; + public once(event: 'closing', listener: () => void): this; + public once(event: 'debug', listener: (message: string) => void): this; + public once(event: 'disconnect', listener: (error: Error) => void): this; + public once(event: 'error', listener: (error: Error) => void): this; + public once(event: 'failed', listener: (error: Error) => void): this; + public once(event: 'newSession', listener: () => void): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'reconnecting', listener: () => void): this; + public once(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public once(event: 'warn', listener: (warning: string | Error) => void): this; + public once(event: string, listener: Function): this; + } + + class VoiceReceiver extends EventEmitter { + constructor(connection: VoiceConnection); + public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm', end?: 'silence' | 'manual' }): Readable; + + public on(event: 'debug', listener: (error: Error | string) => void): this; + public on(event: string, listener: Function): this; + + public once(event: 'debug', listener: (error: Error | string) => void): this; + public once(event: string, listener: Function): this; + } + + export class VoiceRegion { + constructor(data: object); + public custom: boolean; + public deprecated: boolean; + public id: string; + public name: string; + public optimal: boolean; + public sampleHostname: string; + public vip: boolean; + public toJSON(): object; + } + + class VolumeInterface extends EventEmitter { + constructor(options?: { volume?: number }) + public readonly volume: number; + public readonly volumeDecibels: number; + public readonly volumeEditable: boolean; + public readonly volumeLogarithmic: number; + public setVolume(volume: number): void; + public setVolumeDecibels(db: number): void; + public setVolumeLogarithmic(value: number): void; + + public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + + public once(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + } + + export class Webhook extends WebhookMixin() { + constructor(client: Client, data: object); + public avatar: string; + public channelID: Snowflake; + public guildID: Snowflake; + public name: string; + public owner: User | object; + } + + export class WebhookClient extends WebhookMixin(BaseClient) { + constructor(id: string, token: string, options?: ClientOptions); + } + +//#endregion + +//#region Stores + + export class ChannelStore extends DataStore { + constructor(client: Client, iterable: Iterable, options?: { lru: boolean }); + constructor(client: Client, options?: { lru: boolean }); + } + + export class ClientPresenceStore extends PresenceStore { + public setClientPresence(data: PresenceData): Promise; + } + + export class DataStore, R = any> extends Collection { + constructor(client: Client, iterable: Iterable, holds: VConstructor); + public static readonly [Symbol.species]: typeof Collection; + public client: Client; + public holds: VConstructor; + public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; + public remove(key: K): void; + public resolve(resolvable: R): V; + public resolveID(resolvable: R): K; + } + + export class GuildEmojiRoleStore extends OverridableDataStore { + constructor(emoji: GuildEmoji); + public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; + public set(roles: RoleResolvable[] | Collection): Promise; + public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; + } + + export class GuildEmojiStore extends DataStore { + constructor(guild: Guild, iterable?: Iterable); + public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; + public resolveIdentifier(emoji: EmojiIdentifierResolvable): string; + } + + export class GuildChannelStore extends DataStore { + constructor(guild: Guild, iterable?: Iterable); + public create(name: string, options?: GuildCreateChannelOptions): Promise; + } + + // Hacky workaround because changing the signature of an overriden method errors + class OverridableDataStore, R = any> extends DataStore { + public add(data: any, cache: any): any; + public set(key: any): any; + } + + export class GuildMemberRoleStore extends OverridableDataStore { + constructor(member: GuildMember); + public readonly hoist: Role; + public readonly color: Role; + public readonly highest: Role; + + public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; + public set(roles: RoleResolvable[] | Collection, reason?: string): Promise; + public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; + } + + export class GuildMemberStore extends DataStore { + constructor(guild: Guild, iterable?: Iterable); + public ban(user: UserResolvable, options?: BanOptions): Promise; + public fetch(options: UserResolvable | FetchMemberOptions): Promise; + public fetch(): Promise; + public fetch(options: FetchMembersOptions): Promise>; + public prune(options?: GuildPruneMembersOptions): Promise; + public unban(user: UserResolvable, reason?: string): Promise; + } + + export class GuildStore extends DataStore { + constructor(client: Client, iterable?: Iterable); + public create(name: string, options?: { region?: string, icon?: BufferResolvable | Base64Resolvable }): Promise; + } + + export class MessageStore extends DataStore { + constructor(channel: TextChannel | DMChannel | GroupDMChannel, iterable?: Iterable); + public fetch(message: Snowflake): Promise; + public fetch(options?: ChannelLogsQueryOptions): Promise>; + public fetchPinned(): Promise>; + } + + export class PresenceStore extends DataStore { + constructor(client: Client, iterable?: Iterable); + } + + export class ReactionStore extends DataStore { + constructor(message: Message, iterable?: Iterable); + public removeAll(): Promise; + } + + export class ReactionUserStore extends DataStore { + constructor(client: Client, iterable: Iterable | undefined, reaction: MessageReaction); + public fetch(options?: { limit?: number, after?: Snowflake, before?: Snowflake }): Promise; + public remove(user?: UserResolvable): Promise; + } + + export class RoleStore extends DataStore { + constructor(guild: Guild, iterable?: Iterable); + public readonly highest: Role; + + public create(options?: { data?: RoleData, reason?: string }): Promise; + } + + export class UserStore extends DataStore { + constructor(client: Client, iterable?: Iterable); + public fetch(id: Snowflake, cache?: boolean): Promise; + } + +//#endregion + +//#region Mixins + + // Model the TextBasedChannel mixin system, allowing application of these fields + // to the classes that use these methods without having to manually add them + // to each of those classes + + type Constructable = new (...args: any[]) => T; + const PartialTextBasedChannel: (Base?: Constructable) => Constructable; + const TextBasedChannel: (Base?: Constructable) => Constructable; + + type PartialTextBasedChannelFields = { + lastMessageID: Snowflake; + lastMessageChannelID: Snowflake; + readonly lastMessage: Message; + acknowledge(): Promise; + send(content?: StringResolvable, options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; + send(options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; + }; + + type TextBasedChannelFields = { + typing: boolean; + typingCount: number; + awaitMessages(filter: CollectorFilter, options?: AwaitMessagesOptions): Promise>; + bulkDelete(messages: Collection | Message[] | number, filterOld?: boolean): Promise>; + createMessageCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; + search(options?: MessageSearchOptions): Promise; + startTyping(count?: number): Promise; + stopTyping(force?: boolean): void; + } & PartialTextBasedChannelFields; + + const WebhookMixin: (Base?: Constructable) => Constructable; + + type WebhookFields = { + readonly client: Client; + id: Snowflake; + token: string; + delete(reason?: string): Promise; + edit(options: WebhookEditData): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; + send(options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; + sendSlackMessage(body: object): Promise; + } + +//#endregion + +//#region Typedefs + + type ActivityType = 'PLAYING' + | 'STREAMING' + | 'LISTENING' + | 'WATCHING'; + + type APIErrror = { + UNKNOWN_ACCOUNT: number; + UNKNOWN_APPLICATION: number; + UNKNOWN_CHANNEL: number; + UNKNOWN_GUILD: number; + UNKNOWN_INTEGRATION: number; + UNKNOWN_INVITE: number; + UNKNOWN_MEMBER: number; + UNKNOWN_MESSAGE: number; + UNKNOWN_OVERWRITE: number; + UNKNOWN_PROVIDER: number; + UNKNOWN_ROLE: number; + UNKNOWN_TOKEN: number; + UNKNOWN_USER: number; + UNKNOWN_EMOJI: number; + BOT_PROHIBITED_ENDPOINT: number; + BOT_ONLY_ENDPOINT: number; + MAXIMUM_GUILDS: number; + MAXIMUM_FRIENDS: number; + MAXIMUM_PINS: number; + MAXIMUM_ROLES: number; + MAXIMUM_REACTIONS: number; + UNAUTHORIZED: number; + MISSING_ACCESS: number; + INVALID_ACCOUNT_TYPE: number; + CANNOT_EXECUTE_ON_DM: number; + EMBED_DISABLED: number; + CANNOT_EDIT_MESSAGE_BY_OTHER: number; + CANNOT_SEND_EMPTY_MESSAGE: number; + CANNOT_MESSAGE_USER: number; + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: number; + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: number; + OAUTH2_APPLICATION_BOT_ABSENT: number; + MAXIMUM_OAUTH2_APPLICATIONS: number; + INVALID_OAUTH_STATE: number; + MISSING_PERMISSIONS: number; + INVALID_AUTHENTICATION_TOKEN: number; + NOTE_TOO_LONG: number; + INVALID_BULK_DELETE_QUANTITY: number; + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: number; + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: number; + BULK_DELETE_MESSAGE_TOO_OLD: number; + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: number; + REACTION_BLOCKED: number; + } + + type AddGuildMemberOptions = { + accessToken: String; + nick?: string; + roles?: Collection | RoleResolvable[]; + mute?: boolean; + deaf?: boolean; + }; + + type AuditLogChange = { + key: string; + old?: any; + new?: any; + }; + + type AvatarOptions = { + format?: ImageExt; + size?: ImageSize; + }; + + type AwaitMessagesOptions = MessageCollectorOptions & { errors?: string[] }; + + type AwaitReactionsOptions = ReactionCollectorOptions & { errors?: string[] }; + + type BanOptions = { + days?: number; + reason?: string; + }; + + type Base64Resolvable = Buffer | Base64String; + + type Base64String = string; + + type BufferResolvable = Buffer | string; + + type ChannelCreationOverwrites = { + allow?: PermissionResolvable[] | number; + deny?: PermissionResolvable[] | number; + id: RoleResolvable | UserResolvable; + } + + type ChannelData = { + name?: string; + position?: number; + topic?: string; + nsfw?: boolean; + bitrate?: number; + userLimit?: number; + parentID?: Snowflake; + lockPermissions?: boolean; + permissionOverwrites?: PermissionOverwrites[]; + }; + + type ChannelLogsQueryOptions = { + limit?: number + before?: Snowflake + after?: Snowflake + around?: Snowflake + }; + + type ChannelPosition = { + channel: ChannelResolvable; + position: number; + }; + + type ChannelResolvable = Channel | Snowflake; + + type ClientApplicationAsset = { + name: string; + id: Snowflake; + type: 'BIG' | 'SMALL'; + }; + + type ClientApplicationCreateAssetOptions = { + name: string; + data: Base64Resolvable; + type: 'big' | 'small'; + }; + + type ClientOptions = { + apiRequestMethod?: 'sequential' | 'burst'; + presence?: PresenceData; + shardId?: number; + shardCount?: number; + messageCacheMaxSize?: number; + messageCacheLifetime?: number; + messageSweepInterval?: number; + fetchAllMembers?: boolean; + disableEveryone?: boolean; + sync?: boolean; + restWsBridgeTimeout?: number; + restTimeOffset?: number; + disabledEvents?: WSEventType[]; + ws?: WebSocketOptions; + http?: HTTPOptions; + }; + + type CollectorFilter = (...args: any[]) => boolean; + type CollectorOptions = { + time?: number; + dispose?: boolean; + }; + + type ColorResolvable = ('DEFAULT' + | 'AQUA' + | 'GREEN' + | 'BLUE' + | 'PURPLE' + | 'LUMINOUS_VIVID_PINK' + | 'GOLD' + | 'ORANGE' + | 'RED' + | 'GREY' + | 'DARKER_GREY' + | 'NAVY' + | 'DARK_AQUA' + | 'DARK_GREEN' + | 'DARK_BLUE' + | 'DARK_PURPLE' + | 'DARK_VIVID_PINK' + | 'DARK_GOLD' + | 'DARK_ORANGE' + | 'DARK_RED' + | 'DARK_GREY' + | 'LIGHT_GREY' + | 'DARK_NAVY' + | 'RANDOM') + | [number, number, number] + | number + | string; + + type DeconstructedSnowflake = { + timestamp: number; + readonly date: Date; + workerID: number; + processID: number; + increment: number; + binary: string; + }; + + type DefaultMessageNotifications = 'ALL' | 'MENTIONS'; + + type GuildEmojiEditData = { + name?: string; + roles?: Collection | RoleResolvable[]; + }; + + type EmojiIdentifierResolvable = string | EmojiResolvable; + + type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; + + type Extendable = { + GuildEmoji: typeof GuildEmoji; + DMChannel: typeof DMChannel; + GroupDMChannel: typeof GroupDMChannel; + TextChannel: typeof TextChannel; + VoiceChannel: typeof VoiceChannel; + CategoryChannel: typeof CategoryChannel; + GuildChannel: typeof GuildChannel; + GuildMember: typeof GuildMember; + Guild: typeof Guild; + Message: typeof Message; + MessageReaction: typeof MessageReaction; + Presence: typeof Presence; + Role: typeof Role; + User: typeof User; + } + + type FetchMemberOptions = { + user: UserResolvable; + cache?: boolean; + } + + type FetchMembersOptions = { + query?: string; + limit?: number; + } + + type FileOptions = { + attachment: BufferResolvable; + name?: string; + }; + + type GroupActivity = { + partyID: string; + type: number; + }; + + type GroupDMRecipientOptions = { + user?: UserResolvable | Snowflake; + accessToken?: string; + nick?: string; + id?: Snowflake; + }; + + type GuildAuditLogsAction = keyof GuildAuditLogsActions; + + type GuildAuditLogsActions = { + ALL?: null, + GUILD_UPDATE?: number, + CHANNEL_CREATE?: number, + CHANNEL_UPDATE?: number, + CHANNEL_DELETE?: number, + CHANNEL_OVERWRITE_CREATE?: number, + CHANNEL_OVERWRITE_UPDATE?: number, + CHANNEL_OVERWRITE_DELETE?: number, + MEMBER_KICK?: number, + MEMBER_PRUNE?: number, + MEMBER_BAN_ADD?: number, + MEMBER_BAN_REMOVE?: number, + MEMBER_UPDATE?: number, + MEMBER_ROLE_UPDATE?: number, + ROLE_CREATE?: number, + ROLE_UPDATE?: number, + ROLE_DELETE?: number, + INVITE_CREATE?: number, + INVITE_UPDATE?: number, + INVITE_DELETE?: number, + WEBHOOK_CREATE?: number, + WEBHOOK_UPDATE?: number, + WEBHOOK_DELETE?: number, + EMOJI_CREATE?: number, + EMOJI_UPDATE?: number, + EMOJI_DELETE?: number, + MESSAGE_DELETE?: number, + }; + + type GuildAuditLogsActionType = 'CREATE' + | 'DELETE' + | 'UPDATE' + | 'ALL'; + + type GuildAuditLogsFetchOptions = { + before?: Snowflake | GuildAuditLogsEntry; + after?: Snowflake | GuildAuditLogsEntry; + limit?: number; + user?: UserResolvable; + type?: string | number; + }; + + type GuildAuditLogsTarget = keyof GuildAuditLogsTargets; + + type GuildAuditLogsTargets = { + ALL?: string; + GUILD?: string; + CHANNEL?: string; + USER?: string; + ROLE?: string; + INVITE?: string; + WEBHOOK?: string; + EMOJI?: string; + MESSAGE?: string; + }; + + type GuildChannelCloneOptions = { + bitrate?: number; + name?: string; + nsfw?: boolean; + parent?: ChannelResolvable; + reason?: string; + userLimit?: number; + withPermissions?: boolean; + withTopic?: boolean; + }; + + type GuildChannelResolvable = Snowflake | GuildChannel; + + type GuildCreateChannelOptions = { + type?: 'text' | 'voice' | 'category' + nsfw?: boolean; + bitrate?: number; + userLimit?: number; + parent?: ChannelResolvable; + overwrites?: (PermissionOverwrites | ChannelCreationOverwrites)[]; + reason?: string + }; + + type GuildEmojiCreateOptions = { + roles?: Collection | RoleResolvable[]; + reason?: string; + }; + + type GuildChannelMessageNotifications = MessageNotifications + | 'INHERIT'; + + type GuildEditData = { + name?: string; + region?: string; + verificationLevel?: number; + explicitContentFilter?: number; + defaultMessageNotifications?: DefaultMessageNotifications | number; + afkChannel?: ChannelResolvable; + systemChannel?: ChannelResolvable; + afkTimeout?: number; + icon?: Base64Resolvable; + owner?: GuildMemberResolvable; + splash?: Base64Resolvable; + }; + + type GuildFeatures = 'INVITE_SPLASH' + | 'MORE_EMOJI' + | 'VERIFIED' + | 'VIP_REGIONS' + | 'VANITY_URL'; + + type GuildMemberEditData = { + nick?: string; + roles?: Collection | RoleResolvable[]; + mute?: boolean; + deaf?: boolean; + channel?: ChannelResolvable; + }; + + type GuildMemberResolvable = GuildMember | UserResolvable; + + type GuildResolvable = Guild | Snowflake; + + type GuildPruneMembersOptions = { + days?: number; + dry?: boolean; + reason?: string; + }; + + type HTTPOptions = { + version?: number; + host?: string; + cdn?: string; + invite?: string; + }; + + type ImageExt = 'webp' + | 'png' + | 'jpg' + | 'gif'; + + type ImageSize = 128 + | 256 + | 512 + | 1024 + | 2048; + + type InviteOptions = { + temporary?: boolean; + maxAge?: number; + maxUses?: number; + unique?: boolean; + reason?: string; + }; + + type InviteResolvable = string; + + type MessageCollectorOptions = CollectorOptions & { + max?: number; + maxProcessed?: number; + }; + + type MessageEditOptions = { + content?: string; + embed?: MessageEmbedOptions | null; + code?: string | boolean; + }; + + type MessageEmbedOptions = { + title?: string; + description?: string; + url?: string; + timestamp?: Date | number; + color?: number | string; + fields?: { name: string; value: string; inline?: boolean; }[]; + files?: (MessageAttachment | string | FileOptions)[]; + author?: { name?: string; url?: string; icon_url?: string; iconURL?: string; }; + thumbnail?: { url?: string; height?: number; width?: number; }; + image?: { url?: string; proxy_url?: string; proxyURL?: string; height?: number; width?: number; }; + video?: { url?: string; height?: number; width?: number; }; + footer?: { text?: string; icon_url?: string; iconURL?: string; }; + }; + + type MessageNotifications = 'EVERYTHING' + | 'MENTIONS' + | 'NOTHING'; + + type MessageOptions = { + tts?: boolean; + nonce?: string; + content?: string; + embed?: MessageEmbed | MessageEmbedOptions, + disableEveryone?: boolean; + files?: (FileOptions | BufferResolvable | MessageAttachment)[]; + code?: string | boolean; + split?: boolean | SplitOptions; + reply?: UserResolvable; + }; + + type MessageReactionResolvable = MessageReaction | Snowflake; + + type MessageResolvable = Message | Snowflake; + + type MessageSearchOptions = { + content?: string; + maxID?: Snowflake; + minID?: Snowflake; + has?: 'link' + | 'embed' + | 'file' + | 'video' + | 'image' + | 'sound' + | '-link' + | '-embed' + | '-file' + | '-video' + | '-image' + | '-sound'; + channel?: ChannelResolvable; + author?: UserResolvable; + authorType?: 'user' + | 'bot' + | 'webhook' + | '-user' + | '-bot' + | '-webhook'; + sortBy?: 'relevant' | 'timestamp'; + sortOrder?: 'asc' | 'desc'; + contextSize?: number; + limit?: number; + offset?: number; + mentions?: UserResolvable; + mentionsEveryone?: boolean; + linkHostname?: string; + embedProvider?: string; + embedType?: 'image' + | 'video' + | 'url' + | 'rich' + | '-image' + | '-video' + | '-url' + | '-rich'; + attachmentFilename?: string; + attachmentExtension?: string; + before?: Date; + after?: Date; + during?: Date; + nsfw?: boolean; + }; + + type MessageSearchResult = { + total: number; + results: Message[][]; + } + + type MessageType = 'DEFAULT' + | 'RECIPIENT_ADD' + | 'RECIPIENT_REMOVE' + | 'CALL' + | 'CHANNEL_NAME_CHANGE' + | 'CHANNEL_ICON_CHANGE' + | 'PINS_ADD' + | 'GUILD_MEMBER_JOIN'; + + type OverwriteData = { + id: Snowflake; + type: string; + allow?: string; + deny?: string; + } + + type OverwriteType = 'member' | 'role'; + + type PermissionFlags = Record; + + type PermissionObject = Record; + + type PermissionString = 'CREATE_INSTANT_INVITE' + | 'KICK_MEMBERS' + | 'BAN_MEMBERS' + | 'ADMINISTRATOR' + | 'MANAGE_CHANNELS' + | 'MANAGE_GUILD' + | 'ADD_REACTIONS' + | 'VIEW_AUDIT_LOG' + | 'PRIORITY_SPEAKER' + | 'VIEW_CHANNEL' + | 'SEND_MESSAGES' + | 'SEND_TTS_MESSAGES' + | 'MANAGE_MESSAGES' + | 'EMBED_LINKS' + | 'ATTACH_FILES' + | 'READ_MESSAGE_HISTORY' + | 'MENTION_EVERYONE' + | 'USE_EXTERNAL_EMOJIS' + | 'CONNECT' + | 'SPEAK' + | 'MUTE_MEMBERS' + | 'DEAFEN_MEMBERS' + | 'MOVE_MEMBERS' + | 'USE_VAD' + | 'CHANGE_NICKNAME' + | 'MANAGE_NICKNAMES' + | 'MANAGE_ROLES' + | 'MANAGE_WEBHOOKS' + | 'MANAGE_EMOJIS'; + + interface RecursiveArray extends Array> { } + + type PermissionResolvable = RecursiveArray | Permissions | PermissionString | number; + + type PermissionOverwriteOptions = { + allowed: PermissionResolvable; + denied: PermissionResolvable; + id: UserResolvable | RoleResolvable; + } + + type PresenceData = { + status?: PresenceStatus; + afk?: boolean; + activity?: { + name?: string; + type?: ActivityType | number; + url?: string; + } + } + + type PresenceResolvable = Presence | UserResolvable | Snowflake; + + type PresenceStatus = 'online' | 'idle' | 'invisible' | 'dnd'; + + type RateLimitData = { + timeout: number; + limit: number; + timeDifference: number; + method: string; + path: string; + route: string; + } + + type ReactionCollectorOptions = CollectorOptions & { + max?: number; + maxEmojis?: number; + maxUsers?: number; + }; + + type RoleData = { + name?: string; + color?: ColorResolvable; + hoist?: boolean; + position?: number; + permissions?: PermissionResolvable; + mentionable?: boolean; + }; + + type RoleResolvable = Role | string; + + type Snowflake = string; + + type SplitOptions = { + maxLength?: number; + char?: string; + prepend?: string; + append?: string; + }; + + type Status = number; + + type StreamOptions = { + type?: StreamType; + seek?: number; + volume?: number; + passes?: number; + plp?: number; + fec?: boolean; + bitrate?: number | 'auto' + highWaterMark?: number; + }; + + type StreamType = 'unknown' | 'converted' | 'opus' | 'ogg/opus' | 'webm/opus'; + + type StringResolvable = string | string[] | any; + + type UserFlags = 'STAFF' | 'PARTNER' | 'HYPESQUAD'; + + type UserResolvable = User | Snowflake | Message | GuildMember; + + type VoiceStatus = number; + + type WebhookEditData = { + name?: string; + avatar?: BufferResolvable; + channel?: ChannelResolvable; + reason?: string; + }; + + type WebhookMessageOptions = { + username?: string; + avatarURL?: string; + tts?: boolean; + nonce?: string; + embeds?: (MessageEmbed | object)[]; + disableEveryone?: boolean; + files?: (FileOptions | BufferResolvable | MessageAttachment)[]; + code?: string | boolean; + split?: boolean | SplitOptions; + }; + + type WebSocketOptions = { + large_threshold?: number; + compress?: boolean; + }; + + type WSEventType = 'READY' + | 'RESUMED' + | 'GUILD_SYNC' + | 'GUILD_CREATE' + | 'GUILD_DELETE' + | 'GUILD_UPDATE' + | 'GUILD_MEMBER_ADD' + | 'GUILD_MEMBER_REMOVE' + | 'GUILD_MEMBER_UPDATE' + | 'GUILD_MEMBERS_CHUNK' + | 'GUILD_ROLE_CREATE' + | 'GUILD_ROLE_DELETE' + | 'GUILD_ROLE_UPDATE' + | 'GUILD_BAN_ADD' + | 'GUILD_BAN_REMOVE' + | 'GUILD_EMOJIS_UPDATE' + | 'CHANNEL_CREATE' + | 'CHANNEL_DELETE' + | 'CHANNEL_UPDATE' + | 'CHANNEL_PINS_UPDATE' + | 'MESSAGE_CREATE' + | 'MESSAGE_DELETE' + | 'MESSAGE_UPDATE' + | 'MESSAGE_DELETE_BULK' + | 'MESSAGE_REACTION_ADD' + | 'MESSAGE_REACTION_REMOVE' + | 'MESSAGE_REACTION_REMOVE_ALL' + | 'USER_UPDATE' + | 'USER_NOTE_UPDATE' + | 'USER_SETTINGS_UPDATE' + | 'USER_GUILD_SETTINGS_UPDATE' + | 'PRESENCE_UPDATE' + | 'VOICE_STATE_UPDATE' + | 'TYPING_START' + | 'VOICE_SERVER_UPDATE' + | 'RELATIONSHIP_ADD' + | 'RELATIONSHIP_REMOVE'; + +//#endregion +} From 7864653df726ab08e2f71313b69a8e5dd0b6d7e1 Mon Sep 17 00:00:00 2001 From: zajrik Date: Mon, 13 Aug 2018 20:54:36 -0500 Subject: [PATCH 0805/1359] Add missing EOF newlines --- tsconfig.json | 2 +- tslint.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 63656868b..37aa901ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,4 +10,4 @@ "files": [ "typings/index.d.ts" ] -} \ No newline at end of file +} diff --git a/tslint.json b/tslint.json index 6cc6f6b26..71f850264 100644 --- a/tslint.json +++ b/tslint.json @@ -59,4 +59,4 @@ "check-type" ] } -} \ No newline at end of file +} From e666574f363527507bf5b7c52e53f05ead472f11 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 14 Aug 2018 11:50:11 +0100 Subject: [PATCH 0806/1359] document GuildMember#voice --- src/structures/GuildMember.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 9b6beee74..3f0a40cf5 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -96,6 +96,11 @@ class GuildMember extends Base { return (channel && channel.messages.get(this.lastMessageID)) || null; } + /** + * The voice state of this member + * @type {VoiceState} + * @readonly + */ get voice() { return this.guild.voiceStates.get(this.id) || new VoiceState(this.guild, { user_id: this.id }); } From e0f52162eabc901d994f644ba9dcdcc3bf70776d Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 14 Aug 2018 12:12:59 +0100 Subject: [PATCH 0807/1359] voice: allow for streaming silence to avoid audio glitches with repeated pausing/resuming (#2354) --- .../voice/dispatcher/StreamDispatcher.js | 31 ++++++++++++++----- src/client/voice/util/Silence.js | 11 +++++++ 2 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 src/client/voice/util/Silence.js diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index e79531b02..04e32f3a9 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -2,6 +2,7 @@ const VolumeInterface = require('../util/VolumeInterface'); const { Writable } = require('stream'); const secretbox = require('../util/Secretbox'); +const Silence = require('../util/Silence'); const FRAME_LENGTH = 20; const CHANNELS = 2; @@ -41,6 +42,7 @@ class StreamDispatcher extends Writable { this.player = player; this.streamOptions = streamOptions; this.streams = streams; + this.streams.silence = new Silence(); this._nonce = 0; this._nonceBuffer = Buffer.alloc(24); @@ -59,6 +61,7 @@ class StreamDispatcher extends Writable { this.broadcast = this.streams.broadcast; this._pausedTime = 0; + this._silentPausedTime = 0; this.count = 0; this.on('finish', () => { @@ -119,12 +122,17 @@ class StreamDispatcher extends Writable { /** * Pauses playback + * @param {boolean} [silence=false] Whether to play silence while paused to prevent audio glitches */ - pause() { + pause(silence = false) { if (this.paused) return; if (this.streams.opus) this.streams.opus.unpipe(this); - if (this._writeCallback) this._writeCallback(); - this._setSpeaking(false); + if (silence) { + this.streams.silence.pipe(this); + this._silence = true; + } else { + this._setSpeaking(false); + } this.pausedSince = Date.now(); } @@ -138,15 +146,23 @@ class StreamDispatcher extends Writable { * Total time that this dispatcher has been paused * @type {number} */ - get pausedTime() { return this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0); } + get pausedTime() { + return this._silentPausedTime + this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0); + } /** * Resumes playback */ resume() { if (!this.pausedSince) return; + this.streams.silence.unpipe(this); if (this.streams.opus) this.streams.opus.pipe(this); - this._pausedTime += Date.now() - this.pausedSince; + if (this._silence) { + this._silentPausedTime += Date.now() - this.pausedSince; + this._silence = false; + } else { + this._pausedTime += Date.now() - this.pausedSince; + } this.pausedSince = null; if (typeof this._writeCallback === 'function') this._writeCallback(); } @@ -207,11 +223,10 @@ class StreamDispatcher extends Writable { this._writeCallback = null; done(); }; - if (this.pausedSince) return; if (!this.streams.broadcast) { - const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime); + const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this._pausedTime); setTimeout(() => { - if (!this.pausedSince && this._writeCallback) this._writeCallback(); + if ((!this.pausedSince || this._silence) && this._writeCallback) this._writeCallback(); }, next); } this._sdata.sequence++; diff --git a/src/client/voice/util/Silence.js b/src/client/voice/util/Silence.js new file mode 100644 index 000000000..b9643da17 --- /dev/null +++ b/src/client/voice/util/Silence.js @@ -0,0 +1,11 @@ +const { Readable } = require('stream'); + +const SILENCE_FRAME = Buffer.from([0xF8, 0xFF, 0xFE]); + +class Silence extends Readable { + _read() { + this.push(SILENCE_FRAME); + } +} + +module.exports = Silence; From 0ad7c24aa394249a5213a8e11a826821d4b56266 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 14 Aug 2018 12:09:10 -0500 Subject: [PATCH 0808/1359] docs(User): fix hydrabolt's tag (#2747) --- src/structures/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/User.js b/src/structures/User.js index 26a08a853..1fb23d4cf 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -141,7 +141,7 @@ class User extends Base { } /** - * The Discord "tag" (e.g. `hydrabolt#0086`) for this user + * The Discord "tag" (e.g. `hydrabolt#0001`) for this user * @type {string} * @readonly */ From 9b5df571b36010c2c936f53bfb04550e4adbd711 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 14 Aug 2018 20:29:13 +0200 Subject: [PATCH 0809/1359] refactor: typings for voice state and presences refactor Commits: https://github.com/discordjs/discord.js/compare/ea4375bf9051bebdf25411d2eef096bcf148168b...e0f52162eabc901d994f644ba9dcdcc3bf70776d PR: N/A --- typings/index.d.ts | 49 ++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 07b674f04..0170ebf4c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -77,7 +77,6 @@ declare module 'discord.js' { public toString(): string; } - export class Client extends BaseClient { constructor(options?: ClientOptions); private readonly _pingTimestamp: number; @@ -95,7 +94,6 @@ declare module 'discord.js' { public guilds: GuildStore; public readonly ping: number; public pings: number[]; - public presences: ClientPresenceStore; public readyAt: Date; public readonly readyTimestamp: number; public shard: ShardClientUtil; @@ -133,12 +131,13 @@ declare module 'discord.js' { public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; - public on(event: 'guildMemberUpdate' | 'presenceUpdate' | 'voiceStateUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready' | 'reconnecting', listener: () => void): this; public on(event: 'resumed', listener: (replayed: number) => void): this; @@ -147,6 +146,7 @@ declare module 'discord.js' { public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -165,12 +165,13 @@ declare module 'discord.js' { public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; - public once(event: 'guildMemberUpdate' | 'presenceUpdate' | 'voiceStateUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready' | 'reconnecting', listener: () => void): this; public once(event: 'resumed', listener: (replayed: number) => void): this; @@ -179,6 +180,7 @@ declare module 'discord.js' { public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: string, listener: Function): this; } @@ -580,7 +582,6 @@ declare module 'discord.js' { export class GuildMember extends PartialTextBasedChannel(Base) { constructor(client: Client, data: object, guild: Guild); public readonly bannable: boolean; - public readonly deaf: boolean; public deleted: boolean; public readonly displayColor: number; public readonly displayHexColor: string; @@ -591,20 +592,12 @@ declare module 'discord.js' { public joinedTimestamp: number; public readonly kickable: boolean; public readonly manageable: boolean; - public readonly mute: boolean; public nickname: string; public readonly permissions: Permissions; public readonly presence: Presence; public roles: GuildMemberRoleStore; - public readonly selfDeaf: boolean; - public readonly selfMute: boolean; - public readonly serverDeaf: boolean; - public readonly serverMute: boolean; - public readonly speaking: boolean; public user: User; - public readonly voiceChannel: VoiceChannel; - public readonly voiceChannelID: Snowflake; - public readonly voiceSessionID: string; + public readonly voice: VoiceState; public ban(options?: BanOptions): Promise; public createDM(): Promise; public deleteDM(): Promise; @@ -832,6 +825,8 @@ declare module 'discord.js' { constructor(client: Client, data: object); public activity: Activity; public status: 'online' | 'offline' | 'idle' | 'dnd'; + public readonly user: User; + public readonly member?: GuildMember; public equals(presence: Presence): boolean; } @@ -1015,7 +1010,7 @@ declare module 'discord.js' { public setBitrate(value: number | 'auto'): boolean; public setPLP(value: number): boolean; public setFEC(enabled: boolean): boolean; - public pause(): void; + public pause(silence?: boolean): void; public resume(): void; public on(event: 'close', listener: () => void): this; @@ -1265,6 +1260,26 @@ declare module 'discord.js' { public toJSON(): object; } + export class VoiceState extends Base { + constructor(guild: Guild, data: object); + public readonly channel?: VoiceChannel; + public channelID?: Snowflake; + public readonly deaf?: boolean; + public guild: Guild; + public id: Snowflake; + public readonly member: GuildMember; + public readonly mute?: boolean; + public selfDeaf?: boolean; + public selfMute?: boolean; + public serverDeaf?: boolean; + public serverMute?: boolean; + public sessionID?: string; + public readonly speaking?: boolean; + + public setDeaf(mute: boolean, reason?: string): Promise; + public setMute(mute: boolean, reason?: string): Promise; + } + class VolumeInterface extends EventEmitter { constructor(options?: { volume?: number }) public readonly volume: number; @@ -1302,10 +1317,6 @@ declare module 'discord.js' { constructor(client: Client, options?: { lru: boolean }); } - export class ClientPresenceStore extends PresenceStore { - public setClientPresence(data: PresenceData): Promise; - } - export class DataStore, R = any> extends Collection { constructor(client: Client, iterable: Iterable, holds: VConstructor); public static readonly [Symbol.species]: typeof Collection; From 75254748b1ca05bf6e3e2ae571bc7e2e4b6b2c16 Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 14 Aug 2018 20:39:56 +0200 Subject: [PATCH 0810/1359] fix(Client): do not redefine _timeouts and _intervals of BaseClient (#2748) Fixes an issue with the process permanently hanging. --- src/client/Client.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index a193587bd..49a91f389 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -140,20 +140,6 @@ class Client extends BaseClient { */ this.pings = []; - /** - * Timeouts set by {@link Client#setTimeout} that are still active - * @type {Set} - * @private - */ - this._timeouts = new Set(); - - /** - * Intervals set by {@link Client#setInterval} that are still active - * @type {Set} - * @private - */ - this._intervals = new Set(); - if (this.options.messageSweepInterval > 0) { this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); } From 6506252054f5e3a72daeaeffde4daece0bda6d7f Mon Sep 17 00:00:00 2001 From: bdistin Date: Wed, 15 Aug 2018 01:50:22 -0500 Subject: [PATCH 0811/1359] fix/refactor: fix GuildMember#presence getter and cleanup (#2751) * Fix guild being a guild and not an AbstractHandler in PresenceUpdate * update the default guild to be a Guild, and not GuildMember * getters return null instead of undefined * fix lint --- .../websocket/packets/handlers/PresenceUpdate.js | 10 ++-------- src/structures/GuildMember.js | 2 +- src/structures/Presence.js | 8 ++++---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js index aa4f8768d..2892d44c1 100644 --- a/src/client/websocket/packets/handlers/PresenceUpdate.js +++ b/src/client/websocket/packets/handlers/PresenceUpdate.js @@ -35,15 +35,9 @@ class PresenceUpdateHandler extends AbstractHandler { }); client.emit(Events.GUILD_MEMBER_AVAILABLE, member); } - if (member) { - if (client.listenerCount(Events.PRESENCE_UPDATE) === 0) { - guild.presences.add(Object.assign(data, { guild: this })); - return; - } - guild.presences.add(Object.assign(data, { guild: this })); + guild.presences.add(Object.assign(data, { guild })); + if (member && client.listenerCount(Events.PRESENCE_UPDATE)) { client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); - } else { - guild.presences.add(Object.assign(data, { guild: this })); } } } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 3f0a40cf5..fccfcdd4a 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -124,7 +124,7 @@ class GuildMember extends Base { user: { id: this.id, }, - guild: this, + guild: this.guild, }); } diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 13d3ff972..3ade96bf7 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -21,18 +21,18 @@ class Presence { /** * The user of this presence - * @type {User} + * @type {?User} */ get user() { - return this.client.users.get(this.userID); + return this.client.users.get(this.userID) || null; } /** * The member of this presence - * @type {GuildMember} + * @type {?GuildMember} */ get member() { - return this.guild.members.get(this.userID); + return this.guild.members.get(this.userID) || null; } patch(data) { From 700201e3fe36651f77fbe06e2424adb90905d19d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 15 Aug 2018 09:58:27 +0200 Subject: [PATCH 0812/1359] fix(GuildAuditLogs): default target to object with target_id (#2742) --- src/structures/GuildAuditLogs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index f01c02c0f..a5212d5b4 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -148,6 +148,7 @@ class GuildAuditLogs { * * An emoji * * An invite * * A webhook + * * An object with an id key if target was deleted * * An object where the keys represent either the new value or the old value * @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook} AuditLogEntryTarget */ @@ -355,7 +356,7 @@ class GuildAuditLogsEntry { } else if (targetType === Targets.MESSAGE) { this.target = guild.client.users.get(data.target_id); } else { - this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id); + this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id) || { id: data.target_id }; } } From 38597de271ade384bdbb70aad60903c252e7d4c9 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 15 Aug 2018 14:02:11 +0100 Subject: [PATCH 0813/1359] voice: patch in default VoiceStateUpdate in case on doesn't exist, cache member --- .../packets/handlers/VoiceStateUpdate.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js index 97b32e816..57d1cf8b8 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -1,6 +1,7 @@ const AbstractHandler = require('./AbstractHandler'); const { Events } = require('../../../../util/Constants'); +const VoiceState = require('../../../../structures/VoiceState'); class VoiceStateUpdateHandler extends AbstractHandler { handle(packet) { @@ -10,14 +11,23 @@ class VoiceStateUpdateHandler extends AbstractHandler { const guild = client.guilds.get(data.guild_id); if (guild) { // Update the state - let oldState = guild.voiceStates.get(data.user_id); - if (oldState) oldState = oldState._clone(); + const oldState = guild.voiceStates.has(data.user_id) ? + guild.voiceStates.get(data.user_id)._clone() : + new VoiceState(guild, { user_id: data.user_id }); + const newState = guild.voiceStates.add(data); - const member = guild.members.get(data.user_id); + + // Get the member + let member = guild.members.get(data.user_id); + if (member && data.member) { + member._patch(data.member); + } else if (data.member && data.member.user && data.member.joined_at) { + member = guild.members.add(data.member); + } + + // Emit event if (member) { - if (member.user.id === client.user.id && data.channel_id) { - client.emit('self.voiceStateUpdate', data); - } + if (member.user.id === client.user.id && data.channel_id) client.emit('self.voiceStateUpdate', data); client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); } } @@ -27,7 +37,7 @@ class VoiceStateUpdateHandler extends AbstractHandler { /** * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate - * @param {?VoiceState} oldState The voice state before the update + * @param {VoiceState} oldState The voice state before the update * @param {VoiceState} newState The voice state after the update */ From 9a95b6a1e81ff31478a69580c549537fca1fc118 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 15 Aug 2018 15:05:13 +0200 Subject: [PATCH 0814/1359] typings(Client): voiceStateUpdate now always has an old state PR: N/A Commit: https://github.com/discordjs/discord.js/commit/38597de271ade384bdbb70aad60903c252e7d4c9 --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0170ebf4c..7f7b59927 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -146,7 +146,7 @@ declare module 'discord.js' { public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; - public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -180,7 +180,7 @@ declare module 'discord.js' { public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; - public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public once(event: string, listener: Function): this; } From 64832abfdbb21041328a7155790764ef73e0b9fa Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 15 Aug 2018 14:25:47 +0100 Subject: [PATCH 0815/1359] voice: clear connect timeout after connected (fixes #2752) --- src/client/voice/VoiceConnection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 33f72a125..9d1fe3e0d 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -420,6 +420,7 @@ class VoiceConnection extends EventEmitter { this.authentication.secretKey = secret; this.status = VoiceStatus.CONNECTED; + clearTimeout(this.connectTimeout); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, * the connection will already be ready. From 97c34b5b6f30f25883567f09246b76ba30536c2c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 16 Aug 2018 11:28:28 +0100 Subject: [PATCH 0816/1359] voice: clean up packet handling --- src/client/voice/VoiceConnection.js | 20 ++++++++----------- .../voice/dispatcher/StreamDispatcher.js | 14 ++++++------- src/client/voice/networking/VoiceUDPClient.js | 2 +- src/client/voice/networking/VoiceWebSocket.js | 8 +++----- src/client/voice/receiver/PacketHandler.js | 8 ++++---- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 9d1fe3e0d..3f48c9315 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -396,29 +396,25 @@ class VoiceConnection extends EventEmitter { * @param {Object} data The received data * @private */ - onReady({ port, ssrc, ip, modes }) { - this.authentication.port = port; - this.authentication.ssrc = ssrc; - for (let mode of modes) { + onReady(data) { + this.authentication = data; + for (let mode of data.modes) { if (SUPPORTED_MODES.includes(mode)) { - this.authentication.encryptionMode = mode; + this.authentication.mode = mode; this.emit('debug', `Selecting the ${mode} mode`); break; } } - this.sockets.udp.createUDPSocket(ip); + this.sockets.udp.createUDPSocket(data.ip); } /** * Invoked when a session description is received. - * @param {string} mode The encryption mode - * @param {string} secret The secret key + * @param {Object} data The received data * @private */ - onSessionDescription(mode, secret) { - this.authentication.encryptionMode = mode; - this.authentication.secretKey = secret; - + onSessionDescription(data) { + Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; clearTimeout(this.connectTimeout); /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 04e32f3a9..e640b1556 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -242,26 +242,26 @@ class StreamDispatcher extends Writable { } _playChunk(chunk) { - if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secretKey) return; + if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secret_key) return; this._setSpeaking(true); this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk)); } _encrypt(buffer) { - const { secretKey, encryptionMode } = this.player.voiceConnection.authentication; - if (encryptionMode === 'xsalsa20_poly1305_lite') { + const { secret_key, mode } = this.player.voiceConnection.authentication; + if (mode === 'xsalsa20_poly1305_lite') { this._nonce++; if (this._nonce > MAX_NONCE_SIZE) this._nonce = 0; this._nonceBuffer.writeUInt32BE(this._nonce, 0); return [ - secretbox.methods.close(buffer, this._nonceBuffer, secretKey), + secretbox.methods.close(buffer, this._nonceBuffer, secret_key), this._nonceBuffer.slice(0, 4), ]; - } else if (encryptionMode === 'xsalsa20_poly1305_suffix') { + } else if (mode === 'xsalsa20_poly1305_suffix') { const random = secretbox.methods.random(24); - return [secretbox.methods.close(buffer, random, secretKey), random]; + return [secretbox.methods.close(buffer, random, secret_key), random]; } else { - return [secretbox.methods.close(buffer, nonce, secretKey)]; + return [secretbox.methods.close(buffer, nonce, secret_key)]; } } diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index 5dbad33c8..f8668be43 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -104,7 +104,7 @@ class VoiceConnectionUDPClient extends EventEmitter { data: { address: packet.address, port: packet.port, - mode: this.voiceConnection.authentication.encryptionMode, + mode: this.voiceConnection.authentication.mode, }, }, }); diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index be187761b..4f3448ef6 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -168,15 +168,13 @@ class VoiceWebSocket extends EventEmitter { break; /* eslint-disable no-case-declarations */ case VoiceOPCodes.SESSION_DESCRIPTION: - const key = new Uint8Array(new ArrayBuffer(packet.d.secret_key.length)); - for (const i in packet.d.secret_key) key[i] = packet.d.secret_key[i]; + packet.d.secret_key = new Uint8Array(packet.d.secret_key); /** * Emitted once the Voice Websocket receives a description of this voice session. - * @param {string} encryptionMode The type of encryption being used - * @param {Uint8Array} secretKey The secret key used for encryption + * @param {Object} packet The received packet * @event VoiceWebSocket#sessionDescription */ - this.emit('sessionDescription', packet.d.mode, key); + this.emit('sessionDescription', packet.d); break; case VoiceOPCodes.CLIENT_CONNECT: this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id); diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index bcc151f38..1ea702c1f 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -32,14 +32,14 @@ class PacketHandler extends EventEmitter { } parseBuffer(buffer) { - const { secretKey, encryptionMode } = this.receiver.connection.authentication; + const { secret_key, mode } = this.receiver.connection.authentication; // Choose correct nonce depending on encryption let end; - if (encryptionMode === 'xsalsa20_poly1305_lite') { + if (mode === 'xsalsa20_poly1305_lite') { buffer.copy(this.nonce, 0, buffer.length - 4); end = buffer.length - 4; - } else if (encryptionMode === 'xsalsa20_poly1305_suffix') { + } else if (mode === 'xsalsa20_poly1305_suffix') { buffer.copy(this.nonce, 0, buffer.length - 24); end = buffer.length - 24; } else { @@ -47,7 +47,7 @@ class PacketHandler extends EventEmitter { } // Open packet - let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secretKey); + let packet = secretbox.methods.open(buffer.slice(12, end), this.nonce, secret_key); if (!packet) return new Error('Failed to decrypt voice packet'); packet = Buffer.from(packet); From e2726f5a9a4b65621f7265dd027aef904763b2ac Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 16 Aug 2018 18:30:54 +0100 Subject: [PATCH 0817/1359] voice: remove createReceiver, just use VoiceConnection.receiver --- src/client/voice/VoiceConnection.js | 28 ++++++------------- src/client/voice/networking/VoiceWebSocket.js | 10 +++---- test/voice.js | 3 +- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 3f48c9315..6682d32ff 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -54,12 +54,6 @@ class VoiceConnection extends EventEmitter { */ this.speaking = false; - /** - * An array of Voice Receivers that have been created for this connection - * @type {VoiceReceiver[]} - */ - this.receivers = []; - /** * The authentication data needed to connect to the voice server * @type {Object} @@ -114,6 +108,12 @@ class VoiceConnection extends EventEmitter { */ this.sockets = {}; + /** + * The voice receiver of this connection + * @type {VoiceReceiver} + */ + this.receiver = null; + this.authenticate(); } @@ -417,6 +417,7 @@ class VoiceConnection extends EventEmitter { Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; clearTimeout(this.connectTimeout); + this.receiver = new VoiceReceiver(this); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, * the connection will already be ready. @@ -446,9 +447,7 @@ class VoiceConnection extends EventEmitter { if (this.status === VoiceStatus.CONNECTED) { this.emit('speaking', user, speaking); if (!speaking) { - for (const receiver of this.receivers) { - receiver.packets._stoppedSpeaking(user_id); - } + this.receiver.packets._stoppedSpeaking(user_id); } } @@ -466,17 +465,6 @@ class VoiceConnection extends EventEmitter { } } - /** - * Creates a VoiceReceiver so you can start listening to voice data. - * It's recommended to only create one of these. - * @returns {VoiceReceiver} - */ - createReceiver() { - const receiver = new VoiceReceiver(this); - this.receivers.push(receiver); - return receiver; - } - play() {} // eslint-disable-line no-empty-function } diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 4f3448ef6..5e0608ced 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -180,12 +180,10 @@ class VoiceWebSocket extends EventEmitter { this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id); break; case VoiceOPCodes.CLIENT_DISCONNECT: - for (const receiver of this.connection.receivers) { - const streamInfo = receiver.packets.streams.get(packet.d.user_id); - if (streamInfo) { - receiver.packets.streams.delete(packet.d.user_id); - streamInfo.stream.push(null); - } + const streamInfo = this.connection.receiver.packets.streams.get(packet.d.user_id); + if (streamInfo) { + this.connection.receiver.packets.streams.delete(packet.d.user_id); + streamInfo.stream.push(null); } break; case VoiceOPCodes.SPEAKING: diff --git a/test/voice.js b/test/voice.js index b4eac0128..70b5c30f7 100644 --- a/test/voice.js +++ b/test/voice.js @@ -41,8 +41,7 @@ client.on('message', m => { const channel = m.guild.channels.get(m.content.split(' ')[1]) || m.member.voice.channel; if (channel && channel.type === 'voice') { channel.join().then(conn => { - const receiver = conn.createReceiver(); - receiver.createStream(m.author, true).on('data', b => console.log(b.toString())); + conn.receiver.createStream(m.author, true).on('data', b => console.log(b.toString())); conn.player.on('error', (...e) => console.log('player', ...e)); if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); From 3f81b613d83140cdf3d99b93ea74d83c52846b2c Mon Sep 17 00:00:00 2001 From: Kyra Date: Thu, 16 Aug 2018 20:51:21 +0200 Subject: [PATCH 0818/1359] fix: Handle async stacks correctly (#2744) * fix: Capture stack traces in APIRouter to preserve async stack * fix: Handle the stack trace better * fix: Check if error is an instance of Error (5XX rejects with Result) * fix: Error.captureStackTrace is not supported in all browsers --- src/rest/APIRouter.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 6e0fb36a9..830c8501b 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -11,13 +11,27 @@ function buildRoute(manager) { get(target, name) { if (reflectors.includes(name)) return () => route.join('/'); if (methods.includes(name)) { + // Preserve async stack + let stackTrace = null; + if (Error.captureStackTrace) { + stackTrace = {}; + Error.captureStackTrace(stackTrace, this.get); + } + return options => manager.request(name, route.join('/'), Object.assign({ versioned: manager.versioned, route: route.map((r, i) => { if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id'; return r; }).join('/'), - }, options)); + }, options)).catch(error => { + if (stackTrace && (error instanceof Error)) { + stackTrace.name = error.name; + stackTrace.message = error.message; + error.stack = stackTrace.stack; + } + throw error; + }); } route.push(name); return new Proxy(noop, handler); From d437cecb3f8d10fa3f726f5806df1e7d972bbbb4 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 17 Aug 2018 02:46:09 -0500 Subject: [PATCH 0819/1359] Fix permission checking for Administrator channel overwrites (#2756) --- src/structures/GuildChannel.js | 6 +++--- src/structures/Message.js | 4 ++-- src/structures/VoiceChannel.js | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 287ee7a87..9467371f9 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -290,7 +290,7 @@ class GuildChannel extends Channel { get members() { const members = new Collection(); for (const member of this.guild.members.values()) { - if (this.permissionsFor(member).has('VIEW_CHANNEL')) { + if (this.permissionsFor(member).has('VIEW_CHANNEL', false)) { members.set(member.id, member); } } @@ -537,7 +537,7 @@ class GuildChannel extends Channel { * @readonly */ get deletable() { - return this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS); + return this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false); } /** @@ -549,7 +549,7 @@ class GuildChannel extends Channel { if (this.client.user.id === this.guild.ownerID) return true; const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; - return permissions.has([Permissions.FLAGS.MANAGE_CHANNELS, Permissions.FLAGS.VIEW_CHANNEL]); + return permissions.has([Permissions.FLAGS.MANAGE_CHANNELS, Permissions.FLAGS.VIEW_CHANNEL], false); } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index a443b234a..96ffacbb7 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -335,7 +335,7 @@ class Message extends Base { */ get deletable() { return !this.deleted && (this.author.id === this.client.user.id || (this.guild && - this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES) + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false) )); } @@ -346,7 +346,7 @@ class Message extends Base { */ get pinnable() { return !this.guild || - this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES); + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false); } /** diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 048342134..aee01b6f0 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -65,7 +65,7 @@ class VoiceChannel extends GuildChannel { * @readonly */ get deletable() { - return super.deletable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT); + return super.deletable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false); } /** @@ -75,8 +75,8 @@ class VoiceChannel extends GuildChannel { */ get joinable() { if (browser) return false; - if (!this.permissionsFor(this.client.user).has('CONNECT')) return false; - if (this.full && !this.permissionsFor(this.client.user).has('MOVE_MEMBERS')) return false; + if (!this.permissionsFor(this.client.user).has('CONNECT', false)) return false; + if (this.full && !this.permissionsFor(this.client.user).has('MOVE_MEMBERS', false)) return false; return true; } @@ -86,7 +86,7 @@ class VoiceChannel extends GuildChannel { * @readonly */ get speakable() { - return this.permissionsFor(this.client.user).has('SPEAK'); + return this.permissionsFor(this.client.user).has('SPEAK', false); } /** From 4999f675cdef5354eea6454bdbf4c568747cdd8f Mon Sep 17 00:00:00 2001 From: Kyra Date: Fri, 17 Aug 2018 16:52:57 +0200 Subject: [PATCH 0820/1359] src: delete leftover UserConnection class (#2758) --- src/index.js | 1 - src/structures/UserConnection.js | 54 -------------------------------- 2 files changed, 55 deletions(-) delete mode 100644 src/structures/UserConnection.js diff --git a/src/index.js b/src/index.js index 5ed3da570..4bb024e31 100644 --- a/src/index.js +++ b/src/index.js @@ -80,7 +80,6 @@ module.exports = { Role: require('./structures/Role'), TextChannel: require('./structures/TextChannel'), User: require('./structures/User'), - UserConnection: require('./structures/UserConnection'), VoiceChannel: require('./structures/VoiceChannel'), VoiceRegion: require('./structures/VoiceRegion'), VoiceState: require('./structures/VoiceState'), diff --git a/src/structures/UserConnection.js b/src/structures/UserConnection.js deleted file mode 100644 index 698e5b039..000000000 --- a/src/structures/UserConnection.js +++ /dev/null @@ -1,54 +0,0 @@ -const Util = require('../util/Util'); - -/** - * Represents a user connection (or "platform identity"). - */ -class UserConnection { - constructor(user, data) { - /** - * The user that owns the connection - * @type {User} - */ - this.user = user; - - this._patch(data); - } - - _patch(data) { - /** - * The type of the connection - * @type {string} - */ - this.type = data.type; - - /** - * The username of the connection account - * @type {string} - */ - this.name = data.name; - - /** - * The id of the connection account - * @type {string} - */ - this.id = data.id; - - /** - * Whether the connection is revoked - * @type {boolean} - */ - this.revoked = data.revoked; - - /** - * Partial server integrations (not yet implemented) - * @type {Object[]} - */ - this.integrations = data.integrations; - } - - toJSON() { - return Util.flatten(this); - } -} - -module.exports = UserConnection; From 5942e8becc893beb8053e6ef9579aaebf0fa3e15 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 17 Aug 2018 16:55:23 +0200 Subject: [PATCH 0821/1359] typings: remove user bots PRs: #2559 #2758 Commits: - https://github.com/discordjs/discord.js/commit/5afd77ab730f413d0090be6ce0b0a7083ef15f7c - https://github.com/discordjs/discord.js/commit/4999f675cdef5354eea6454bdbf4c568747cdd8f --- typings/index.d.ts | 178 +-------------------------------------------- 1 file changed, 3 insertions(+), 175 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 7f7b59927..2b7c5132d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -104,23 +104,18 @@ declare module 'discord.js' { public users: UserStore; public readonly voiceConnections: Collection; public createVoiceBroadcast(): VoiceBroadcast; - public destroy(): Promise; - public fetchApplication(id?: Snowflake): Promise; + public fetchApplication(): Promise; public fetchInvite(invite: InviteResolvable): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhook(id: Snowflake, token?: string): Promise; public generateInvite(permissions?: number | PermissionResolvable[]): Promise; public login(token?: string): Promise; public sweepMessages(lifetime?: number): number; - public syncGuilds(guilds?: Guild[] | Collection): void; public toJSON(): object; public on(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; - public on(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; - public on(event: 'clientUserSettingsUpdate', listener: (clientUserSettings: ClientUserSettings) => void): this; - public on(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; public on(event: 'debug' | 'warn', listener: (info: string) => void): this; public on(event: 'disconnect', listener: (event: any) => void): this; public on(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; @@ -144,7 +139,6 @@ declare module 'discord.js' { public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; - public on(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public on(event: string, listener: Function): this; @@ -152,9 +146,6 @@ declare module 'discord.js' { public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; - public once(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; - public once(event: 'clientUserSettingsUpdate', listener: (clientUserSettings: ClientUserSettings) => void): this; - public once(event: 'clientUserGuildSettingsUpdate', listener: (clientUserGuildSettings: ClientUserGuildSettings) => void): this; public once(event: 'debug' | 'warn', listener: (info: string) => void): this; public once(event: 'disconnect', listener: (event: any) => void): this; public once(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; @@ -178,7 +169,6 @@ declare module 'discord.js' { public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; - public once(event: 'userNoteUpdate', listener: (user: UserResolvable, oldNote: string, newNote: string) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public once(event: string, listener: Function): this; @@ -205,8 +195,6 @@ declare module 'discord.js' { public createAsset(name: string, data: Base64Resolvable, type: 'big' | 'small' | 'Big' | 'Small'): Promise; public fetchAssets(): Promise; public iconURL(options?: AvatarOptions): string; - public resetSecret(): Promise; - public resetToken(): Promise; public toJSON(): object; public toString(): string; } @@ -220,76 +208,15 @@ declare module 'discord.js' { } export class ClientUser extends User { - public blocked: Collection; - public email: string; - public friends: Collection; - public guildSettings: Collection; public mfaEnabled: boolean; - public mobile: boolean; - public notes: Collection; - public premium: boolean; - public settings: ClientUserSettings; public verified: boolean; public createGroupDM(recipients: GroupDMRecipientOptions[]): Promise; - public fetchMentions(options?: { limit?: number; roles?: boolean, everyone?: boolean; guild?: Guild | Snowflake }): Promise; public setActivity(name: string, options?: { url?: string, type?: ActivityType | number }): Promise; public setAFK(afk: boolean): Promise; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; - public setEmail(email: string, password: string): Promise; - public setPassword(newPassword: string, options: { oldPassword?: string, mfaCode?: string }): Promise; public setPresence(data: PresenceData): Promise; public setStatus(status: PresenceStatus): Promise; - public setUsername(username: string, password?: string): Promise; - public toJSON(): object; - } - - export class ClientUserChannelOverride { - constructor(data: object); - private patch(data: object): void; - - public messageNotifications: GuildChannelMessageNotifications; - public muted: boolean; - } - - export class ClientUserGuildSettings { - constructor(client: Client, data: object); - private patch(data: object): void; - private update(name: string, value: any): Promise; - - public channelOverrides: Collection; - public readonly client: Client; - public guildID: Snowflake; - public messageNotifications: GuildChannelMessageNotifications; - public mobilePush: boolean; - public muted: boolean; - public suppressEveryone: boolean; - } - - export class ClientUserSettings { - constructor(user: User, data: object); - private update(name: string, value: any): Promise; - private patch(data: object): void; - - public convertEmoticons: boolean; - public defaultGuildsRestricted: boolean; - public detectPlatformAccounts: boolean; - public developerMode: boolean; - public enableTTSCommand: boolean; - public explicitContentFilter: 'DISABLED' | 'NON_FRIENDS' | 'FRIENDS_AND_NON_FRIENDS'; - public friendsSources: { all: boolean, mutualGuilds: boolean, mutualFriends: boolean }; - public guildsPositions: Snowflake[]; - public inlineAttachmentMedia: boolean; - public inlineEmbedMedia: boolean; - public locale: string; - public messageDisplayCompact: boolean; - public renderReactions: boolean; - public restrictedGuilds: Snowflake[]; - public showCurrentGame: boolean; - public status: PresenceStatus; - public theme: string; - public addRestrictedGuild(guild: Guild): Promise; - public removeRestrictedGuild(guild: Guild): Promise; - public setGuildPosition(guild: Guild, position: number, relative?: boolean): Promise; + public setUsername(username: string): Promise; } export class Collection extends Map { @@ -442,28 +369,21 @@ declare module 'discord.js' { public readonly me: GuildMember; public memberCount: number; public members: GuildMemberStore; - public readonly messageNotifications: MessageNotifications; public mfaLevel: number; - public readonly mobilePush: boolean; - public readonly muted: boolean; public name: string; public readonly nameAcronym: string; public readonly owner: GuildMember; public ownerID: Snowflake; - public readonly position: number; public presences: PresenceStore; public region: string; public roles: RoleStore; public splash: string; - public readonly suppressEveryone: boolean; public readonly systemChannel: TextChannel; public systemChannelID: Snowflake; public verificationLevel: number; public readonly verified: boolean; public readonly voiceConnection: VoiceConnection; - public acknowledge(): Promise; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; - public allowDMs(allow: boolean): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; public equals(guild: Guild): boolean; @@ -475,7 +395,6 @@ declare module 'discord.js' { public iconURL(options?: AvatarOptions): string; public leave(): Promise; public member(user: UserResolvable): GuildMember; - public search(options?: MessageSearchOptions): Promise; public setAFKChannel(afkChannel: ChannelResolvable, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; public setChannelPositions(channelPositions: ChannelPosition[]): Promise; @@ -484,13 +403,11 @@ declare module 'discord.js' { public setIcon(icon: Base64Resolvable, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; - public setPosition(position: number, relative?: boolean): Promise; public setRegion(region: string, reason?: string): Promise; public setSplash(splash: Base64Resolvable, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; public splashURL(options?: AvatarOptions): string; - public sync(): void; public toJSON(): object; public toString(): string; } @@ -534,9 +451,7 @@ declare module 'discord.js' { public readonly calculatedPosition: number; public readonly deletable: boolean; public guild: Guild; - public readonly messageNotifications: GuildChannelMessageNotifications; public readonly manageable: boolean; - public readonly muted: boolean; public name: string; public readonly parent: CategoryChannel; public parentID: Snowflake; @@ -672,7 +587,6 @@ declare module 'discord.js' { public tts: boolean; public type: MessageType; public webhookID: Snowflake; - public acknowledge(): Promise; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; @@ -1069,7 +983,6 @@ declare module 'discord.js' { public readonly dmChannel: DMChannel; public id: Snowflake; public locale: string; - public readonly note: string; public readonly presence: Presence; public readonly tag: string; public username: string; @@ -1078,8 +991,6 @@ declare module 'discord.js' { public deleteDM(): Promise; public displayAvatarURL(options?: AvatarOptions): string; public equals(user: User): boolean; - public fetchProfile(): Promise; - public setNote(note: string): Promise; public toString(): string; public typingDurationIn(channel: ChannelResolvable): number; public typingIn(channel: ChannelResolvable): boolean; @@ -1097,19 +1008,6 @@ declare module 'discord.js' { public toJSON(): object; } - class UserProfile extends Base { - constructor(user: User, data: object); - private _flags: number; - - public connections: Collection; - public readonly flags: UserFlags[]; - public mutualGuilds: Collection; - public premium: boolean; - public premiumSince: Date; - public user: User; - public toJSON(): object; - } - export class Util { public static basename(path: string, ext?: string): string; public static binaryToID(num: string): Snowflake; @@ -1428,7 +1326,6 @@ declare module 'discord.js' { lastMessageID: Snowflake; lastMessageChannelID: Snowflake; readonly lastMessage: Message; - acknowledge(): Promise; send(content?: StringResolvable, options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; send(options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; }; @@ -1439,7 +1336,6 @@ declare module 'discord.js' { awaitMessages(filter: CollectorFilter, options?: AwaitMessagesOptions): Promise>; bulkDelete(messages: Collection | Message[] | number, filterOld?: boolean): Promise>; createMessageCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; - search(options?: MessageSearchOptions): Promise; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; } & PartialTextBasedChannelFields; @@ -1600,7 +1496,6 @@ declare module 'discord.js' { messageSweepInterval?: number; fetchAllMembers?: boolean; disableEveryone?: boolean; - sync?: boolean; restWsBridgeTimeout?: number; restTimeOffset?: number; disabledEvents?: WSEventType[]; @@ -1793,9 +1688,6 @@ declare module 'discord.js' { reason?: string; }; - type GuildChannelMessageNotifications = MessageNotifications - | 'INHERIT'; - type GuildEditData = { name?: string; region?: string; @@ -1888,10 +1780,6 @@ declare module 'discord.js' { footer?: { text?: string; icon_url?: string; iconURL?: string; }; }; - type MessageNotifications = 'EVERYTHING' - | 'MENTIONS' - | 'NOTHING'; - type MessageOptions = { tts?: boolean; nonce?: string; @@ -1908,60 +1796,6 @@ declare module 'discord.js' { type MessageResolvable = Message | Snowflake; - type MessageSearchOptions = { - content?: string; - maxID?: Snowflake; - minID?: Snowflake; - has?: 'link' - | 'embed' - | 'file' - | 'video' - | 'image' - | 'sound' - | '-link' - | '-embed' - | '-file' - | '-video' - | '-image' - | '-sound'; - channel?: ChannelResolvable; - author?: UserResolvable; - authorType?: 'user' - | 'bot' - | 'webhook' - | '-user' - | '-bot' - | '-webhook'; - sortBy?: 'relevant' | 'timestamp'; - sortOrder?: 'asc' | 'desc'; - contextSize?: number; - limit?: number; - offset?: number; - mentions?: UserResolvable; - mentionsEveryone?: boolean; - linkHostname?: string; - embedProvider?: string; - embedType?: 'image' - | 'video' - | 'url' - | 'rich' - | '-image' - | '-video' - | '-url' - | '-rich'; - attachmentFilename?: string; - attachmentExtension?: string; - before?: Date; - after?: Date; - during?: Date; - nsfw?: boolean; - }; - - type MessageSearchResult = { - total: number; - results: Message[][]; - } - type MessageType = 'DEFAULT' | 'RECIPIENT_ADD' | 'RECIPIENT_REMOVE' @@ -2122,7 +1956,6 @@ declare module 'discord.js' { type WSEventType = 'READY' | 'RESUMED' - | 'GUILD_SYNC' | 'GUILD_CREATE' | 'GUILD_DELETE' | 'GUILD_UPDATE' @@ -2148,15 +1981,10 @@ declare module 'discord.js' { | 'MESSAGE_REACTION_REMOVE' | 'MESSAGE_REACTION_REMOVE_ALL' | 'USER_UPDATE' - | 'USER_NOTE_UPDATE' - | 'USER_SETTINGS_UPDATE' - | 'USER_GUILD_SETTINGS_UPDATE' | 'PRESENCE_UPDATE' | 'VOICE_STATE_UPDATE' | 'TYPING_START' - | 'VOICE_SERVER_UPDATE' - | 'RELATIONSHIP_ADD' - | 'RELATIONSHIP_REMOVE'; + | 'VOICE_SERVER_UPDATE'; //#endregion } From 6841b4e66a9783c731ab2bf1b1bee2090b6a140d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 17 Aug 2018 16:58:41 +0200 Subject: [PATCH 0822/1359] typings(VoiceConnection): receivers is now receiver and no longer an array PR: N/A Commit: https://github.com/discordjs/discord.js/commit/e2726f5a9a4b65621f7265dd027aef904763b2ac --- typings/index.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 2b7c5132d..ece49a387 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1100,11 +1100,10 @@ declare module 'discord.js' { public readonly client: Client; public readonly dispatcher: StreamDispatcher; public player: object; - public receivers: VoiceReceiver[]; + public receiver: VoiceReceiver; public speaking: boolean; public status: VoiceStatus; public voiceManager: object; - public createReceiver(): VoiceReceiver; public disconnect(): void; public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; From 418be7bef18c657230cfdb71709e5d966586a6aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 17 Aug 2018 23:01:01 -0500 Subject: [PATCH 0823/1359] build(deps-dev): update tslint-config-typings requirement from ^0.2.4 to ^0.3.1 (#2746) Updates the requirements on [tslint-config-typings](https://github.com/typings/tslint-config-typings) to permit the latest version. - [Release notes](https://github.com/typings/tslint-config-typings/releases) - [Commits](https://github.com/typings/tslint-config-typings/commits/v0.3.1) Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0caadd512..f1b2b948f 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "eslint": "^5.0.1", "json-filter-loader": "^1.0.0", "tslint": "^3.15.1", - "tslint-config-typings": "^0.2.4", + "tslint-config-typings": "^0.3.1", "typescript": "^3.0.1", "uglifyjs-webpack-plugin": "^1.1.8", "webpack": "^4.5.0", From e55f822ad4ce867c288cbd03fc23fdae43479057 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 17 Aug 2018 23:07:08 -0500 Subject: [PATCH 0824/1359] build(deps-dev): update tslint requirement from ^3.15.1 to ^5.11.0 (#2745) Updates the requirements on [tslint](https://github.com/palantir/tslint) to permit the latest version. - [Release notes](https://github.com/palantir/tslint/releases) - [Changelog](https://github.com/palantir/tslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/palantir/tslint/commits/5.11.0) Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1b2b948f..106d5822a 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "discord.js-docgen": "discordjs/docgen", "eslint": "^5.0.1", "json-filter-loader": "^1.0.0", - "tslint": "^3.15.1", + "tslint": "^5.11.0", "tslint-config-typings": "^0.3.1", "typescript": "^3.0.1", "uglifyjs-webpack-plugin": "^1.1.8", From 29d51fa582c834161cdf3076674c3f5ef2548431 Mon Sep 17 00:00:00 2001 From: zajrik Date: Fri, 17 Aug 2018 23:11:06 -0500 Subject: [PATCH 0825/1359] tests: Fix typings lint script Forgot to update it after moving the typings. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 106d5822a..169672412 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "docs:test": "docgen --source src --custom docs/index.yml", "lint": "eslint src *.js", "lint:fix": "eslint --fix src", - "lint:typings": "tslint index.d.ts discord.js-test.ts", + "lint:typings": "tslint typings/index.d.ts typings/discord.js-test.ts", "build:browser": "webpack", "prepublishOnly": "npm run test && NODE_ENV=production npm run build:browser" }, From 5fdd3a5877131e2744f7189fe2097a78e9ec2648 Mon Sep 17 00:00:00 2001 From: zajrik Date: Fri, 17 Aug 2018 23:38:54 -0500 Subject: [PATCH 0826/1359] tests(lint): Fix typings lint errors --- typings/discord.js-test.ts | 2 +- typings/index.d.ts | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/typings/discord.js-test.ts b/typings/discord.js-test.ts index 6792dba87..2ff0fe035 100644 --- a/typings/discord.js-test.ts +++ b/typings/discord.js-test.ts @@ -22,7 +22,7 @@ client.login('dsfsd754.4fds4f68d4f6sd46f4s.7878easfdsgdfFDSIJIO'); export class TestCollector extends Collector { public filter: CollectorFilter; - public constructor(client: Client, filter: CollectorFilter, ) { + public constructor(client: Client, filter: CollectorFilter) { super(client, filter); } diff --git a/typings/index.d.ts b/typings/index.d.ts index ece49a387..bf104615f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -162,7 +162,7 @@ declare module 'discord.js' { public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; - public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; + public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready' | 'reconnecting', listener: () => void): this; public once(event: 'resumed', listener: (replayed: number) => void): this; @@ -242,7 +242,7 @@ declare module 'discord.js' { public lastKey(): K | undefined; public lastKey(count: number): K[]; public map(fn: (value: V, key: K, collection: Collection) => T, thisArg?: any): T[]; - public partition(fn: (value: V, key: K, collection: Collection) => boolean): [Collection, Collection] + public partition(fn: (value: V, key: K, collection: Collection) => boolean): [Collection, Collection]; public random(): V | undefined; public random(count: number): V[]; public randomKey(): K | undefined; @@ -330,7 +330,7 @@ declare module 'discord.js' { public readonly owner: User; public ownerID: Snowflake; public recipients: Collection; - public addUser(options: { user: UserResolvable, accessToken?: string, nick?: string }): Promise + public addUser(options: { user: UserResolvable, accessToken?: string, nick?: string }): Promise; public edit (data: { icon?: string, name?: string }): Promise; public equals(channel: GroupDMChannel): boolean; public iconURL(options?: AvatarOptions): string; @@ -1015,7 +1015,7 @@ declare module 'discord.js' { public static cloneObject(obj: object): object; public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; - public static discordSort(collection: Collection): Collection + public static discordSort(collection: Collection): Collection; public static escapeMarkdown(text: string, onlyCodeBlock?: boolean, onlyInlineCode?: boolean): string; public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise; public static flatten(obj: object, ...props: { [key: string]: boolean | string }[]): object; @@ -1033,7 +1033,7 @@ declare module 'discord.js' { relative: boolean, sorted: Collection, route: object, - reason?: string, + reason?: string ): Promise<{ id: Snowflake; position: number }[]>; public static splitMessage(text: string, options?: SplitOptions): string | string[]; public static str2ab(str: string): ArrayBuffer; @@ -1350,7 +1350,7 @@ declare module 'discord.js' { send(content?: StringResolvable, options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; send(options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; sendSlackMessage(body: object): Promise; - } + }; //#endregion @@ -1405,7 +1405,7 @@ declare module 'discord.js' { BULK_DELETE_MESSAGE_TOO_OLD: number; INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: number; REACTION_BLOCKED: number; - } + }; type AddGuildMemberOptions = { accessToken: String; @@ -1445,7 +1445,7 @@ declare module 'discord.js' { allow?: PermissionResolvable[] | number; deny?: PermissionResolvable[] | number; id: RoleResolvable | UserResolvable; - } + }; type ChannelData = { name?: string; @@ -1571,17 +1571,17 @@ declare module 'discord.js' { Presence: typeof Presence; Role: typeof Role; User: typeof User; - } + }; type FetchMemberOptions = { user: UserResolvable; cache?: boolean; - } + }; type FetchMembersOptions = { query?: string; limit?: number; - } + }; type FileOptions = { attachment: BufferResolvable; @@ -1629,7 +1629,7 @@ declare module 'discord.js' { EMOJI_CREATE?: number, EMOJI_UPDATE?: number, EMOJI_DELETE?: number, - MESSAGE_DELETE?: number, + MESSAGE_DELETE?: number }; type GuildAuditLogsActionType = 'CREATE' @@ -1809,7 +1809,7 @@ declare module 'discord.js' { type: string; allow?: string; deny?: string; - } + }; type OverwriteType = 'member' | 'role'; @@ -1855,7 +1855,7 @@ declare module 'discord.js' { allowed: PermissionResolvable; denied: PermissionResolvable; id: UserResolvable | RoleResolvable; - } + }; type PresenceData = { status?: PresenceStatus; @@ -1865,7 +1865,7 @@ declare module 'discord.js' { type?: ActivityType | number; url?: string; } - } + }; type PresenceResolvable = Presence | UserResolvable | Snowflake; @@ -1878,7 +1878,7 @@ declare module 'discord.js' { method: string; path: string; route: string; - } + }; type ReactionCollectorOptions = CollectorOptions & { max?: number; From e935611e50d3a55c35c9698c88639aae8a2d9b67 Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Sat, 18 Aug 2018 11:53:23 +0300 Subject: [PATCH 0827/1359] fix(GuildBanAddHandler): not emitting guildBanAdd for uncached users (#2763) --- src/client/websocket/packets/handlers/GuildBanAdd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/packets/handlers/GuildBanAdd.js b/src/client/websocket/packets/handlers/GuildBanAdd.js index 940ac7812..89f57b78d 100644 --- a/src/client/websocket/packets/handlers/GuildBanAdd.js +++ b/src/client/websocket/packets/handlers/GuildBanAdd.js @@ -8,7 +8,7 @@ class GuildBanAddHandler extends AbstractHandler { const client = this.packetManager.client; const data = packet.d; const guild = client.guilds.get(data.guild_id); - const user = client.users.get(data.user.id); + const user = client.users.add(data.user); if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); } } From 1e0379375eb0019faaaf54fa531d411aaa2c8816 Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Sat, 18 Aug 2018 11:55:09 +0300 Subject: [PATCH 0828/1359] cleanup: remove more left-over selfbot constants (#2761) * misc: Remove left-over selfbot stuff * Forgot this --- src/util/Constants.js | 154 ------------------------------------------ 1 file changed, 154 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 9b7702e25..ce5427552 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -291,8 +291,6 @@ exports.Events = { * * VOICE_STATE_UPDATE * * TYPING_START * * VOICE_SERVER_UPDATE - * * RELATIONSHIP_ADD - * * RELATIONSHIP_REMOVE * @typedef {string} WSEventType */ exports.WSEvents = keyMirror([ @@ -376,158 +374,6 @@ exports.ActivityFlags = { PLAY: 1 << 5, }; -exports.ExplicitContentFilterTypes = [ - 'DISABLED', - 'NON_FRIENDS', - 'FRIENDS_AND_NON_FRIENDS', -]; - -exports.UserSettingsMap = { - /** - * Automatically convert emoticons in your messages to emoji, - * for example when you type `:-)` Discord will convert it to 😃 - * @name ClientUserSettings#convertEmoticons - * @type {boolean} - */ - convert_emoticons: 'convertEmoticons', - - /** - * If new guilds should automatically disable DMs between you and its members - * @name ClientUserSettings#defaultGuildsRestricted - * @type {boolean} - */ - default_guilds_restricted: 'defaultGuildsRestricted', - - /** - * Automatically detect accounts from services like Steam and Blizzard when you open the Discord client - * @name ClientUserSettings#detectPlatformAccounts - * @type {boolean} - */ - detect_platform_accounts: 'detectPlatformAccounts', - - /** - * Developer Mode exposes context menu items helpful for people writing bots using the Discord API - * @name ClientUserSettings#developerMode - * @type {boolean} - */ - developer_mode: 'developerMode', - - /** - * Allow playback and usage of the `/tts` command - * @name ClientUserSettings#enableTTSCommand - * @type {boolean} - */ - enable_tts_command: 'enableTTSCommand', - - /** - * The theme of the client. Either `light` or `dark` - * @name ClientUserSettings#theme - * @type {string} - */ - theme: 'theme', - - /** - * Last status set in the client - * @name ClientUserSettings#status - * @type {PresenceStatus} - */ - status: 'status', - - /** - * Display currently running game as status message - * @name ClientUserSettings#showCurrentGame - * @type {boolean} - */ - show_current_game: 'showCurrentGame', - - /** - * Display images, videos, and lolcats when uploaded directly to Discord - * @name ClientUserSettings#inlineAttachmentMedia - * @type {boolean} - */ - inline_attachment_media: 'inlineAttachmentMedia', - - /** - * Display images, videos, and lolcats when posted as links in chat - * @name ClientUserSettings#inlineEmbedMedia - * @type {boolean} - */ - inline_embed_media: 'inlineEmbedMedia', - - /** - * Language the Discord client will use, as an RFC 3066 language identifier - * @name ClientUserSettings#locale - * @type {string} - */ - locale: 'locale', - - /** - * Display messages in compact mode - * @name ClientUserSettings#messageDisplayCompact - * @type {boolean} - */ - message_display_compact: 'messageDisplayCompact', - - /** - * Show emoji reactions on messages - * @name ClientUserSettings#renderReactions - * @type {boolean} - */ - render_reactions: 'renderReactions', - - /** - * Array of snowflake IDs for guilds, in the order they appear in the Discord client - * @name ClientUserSettings#guildPositions - * @type {Snowflake[]} - */ - guild_positions: 'guildPositions', - - /** - * Array of snowflake IDs for guilds which you will not recieve DMs from - * @name ClientUserSettings#restrictedGuilds - * @type {Snowflake[]} - */ - restricted_guilds: 'restrictedGuilds', - - explicit_content_filter: function explicitContentFilter(type) { // eslint-disable-line func-name-matching - /** - * Safe direct messaging; force people's messages with images to be scanned before they are sent to you. - * One of `DISABLED`, `NON_FRIENDS`, `FRIENDS_AND_NON_FRIENDS` - * @name ClientUserSettings#explicitContentFilter - * @type {string} - */ - return exports.ExplicitContentFilterTypes[type]; - }, - friend_source_flags: function friendSources(flags) { // eslint-disable-line func-name-matching - /** - * Who can add you as a friend - * @name ClientUserSettings#friendSources - * @type {Object} - * @property {boolean} all Mutual friends and mutual guilds - * @property {boolean} mutualGuilds Only mutual guilds - * @property {boolean} mutualFriends Only mutual friends - */ - return { - all: flags.all || false, - mutualGuilds: flags.all ? true : flags.mutual_guilds || false, - mutualFriends: flags.all ? true : flags.mutualFriends || false, - }; - }, -}; - -/** - * All flags users can have: - * * STAFF - * * PARTNER - * * HYPESQUAD - * @typedef {string} UserFlags - */ -exports.UserFlags = { - STAFF: 1 << 0, - PARTNER: 1 << 1, - HYPESQUAD: 1 << 2, -}; - exports.ChannelTypes = { TEXT: 0, DM: 1, From b2707e9ee5a0ea1ac24068cffefa84323d647989 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 18 Aug 2018 10:57:08 +0200 Subject: [PATCH 0829/1359] cleanup(VoiceRegion): remove no longer existing sampleHostname (#2759) --- src/structures/VoiceRegion.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/structures/VoiceRegion.js b/src/structures/VoiceRegion.js index 68c9e93cf..b4db21770 100644 --- a/src/structures/VoiceRegion.js +++ b/src/structures/VoiceRegion.js @@ -40,12 +40,6 @@ class VoiceRegion { * @type {boolean} */ this.custom = data.custom; - - /** - * A sample hostname for what a connection might look like - * @type {string} - */ - this.sampleHostname = data.sample_hostname; } toJSON() { From c966aa24579a27e7546862810aea3792a5cbd4eb Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 18 Aug 2018 10:58:29 +0200 Subject: [PATCH 0830/1359] typings(VoiceRegion): remove no longer existing sampleHostname --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index bf104615f..9c8f7b3f5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1152,7 +1152,6 @@ declare module 'discord.js' { public id: string; public name: string; public optimal: boolean; - public sampleHostname: string; public vip: boolean; public toJSON(): object; } From 58ce08255f45ebb1eba1802e907adc4cd90a50bc Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 18 Aug 2018 11:07:29 +0200 Subject: [PATCH 0831/1359] typings: remove UserFlags and ClientApplicationCreateAssetsOptions Leftover user bots declarations --- typings/index.d.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 9c8f7b3f5..30c2a22ff 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1478,12 +1478,6 @@ declare module 'discord.js' { type: 'BIG' | 'SMALL'; }; - type ClientApplicationCreateAssetOptions = { - name: string; - data: Base64Resolvable; - type: 'big' | 'small'; - }; - type ClientOptions = { apiRequestMethod?: 'sequential' | 'burst'; presence?: PresenceData; @@ -1922,8 +1916,6 @@ declare module 'discord.js' { type StringResolvable = string | string[] | any; - type UserFlags = 'STAFF' | 'PARTNER' | 'HYPESQUAD'; - type UserResolvable = User | Snowflake | Message | GuildMember; type VoiceStatus = number; From e7ee8d21d7f45e29382515fab431a5515e718720 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 18 Aug 2018 11:07:48 +0200 Subject: [PATCH 0832/1359] typings: VoiceState is extendable --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 30c2a22ff..eb42e6333 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1562,6 +1562,7 @@ declare module 'discord.js' { Message: typeof Message; MessageReaction: typeof MessageReaction; Presence: typeof Presence; + VoiceState: typeof VoiceState; Role: typeof Role; User: typeof User; }; From 48f5a17fc4221769a105b700a26344136b0c07dd Mon Sep 17 00:00:00 2001 From: zajrik Date: Sat, 18 Aug 2018 05:35:09 -0500 Subject: [PATCH 0833/1359] cleanup(typings): Remove discord.js-test.ts Useless file, filled with errors. Never served any real purpose. Discussed on discord. Can be replaced should a time come that we automate testing the typings further than just linting. --- typings/discord.js-test.ts | 69 -------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 typings/discord.js-test.ts diff --git a/typings/discord.js-test.ts b/typings/discord.js-test.ts deleted file mode 100644 index 2ff0fe035..000000000 --- a/typings/discord.js-test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/// - -import { Collector, Message, CollectorFilter, Client, CollectorHandler, MessageReaction, Collection, User, ReactionCollectorOptions, Snowflake } from 'discord.js'; - -const client = new Client({ - disableEveryone: false, - disabledEvents: ['GUILD_MEMBER_ADD'] -}); - -client.on('message', (message) => { - if (message.content === 'hello') { - message.channel.sendMessage('o/'); - } - - const collector: ReactionCollector = new ReactionCollector(message, - (reaction: MessageReaction) => reaction.emoji.toString() === '👌', - { time: 30e3 }); - collector.on('end', collected => console.log(collected)); -}); - -client.login('dsfsd754.4fds4f68d4f6sd46f4s.7878easfdsgdfFDSIJIO'); - -export class TestCollector extends Collector { - public filter: CollectorFilter; - public constructor(client: Client, filter: CollectorFilter) { - super(client, filter); - } - - public handle(message: Message): CollectorHandler { - return { key: message.id, value: message }; - } - - public cleanup(): void {} - public postCheck(): null { return null; } -} - -class ReactionCollector extends Collector { - public message: Message; - public users: Collection; - public total: number; - public options: ReactionCollectorOptions; - public constructor(message: Message, filter: CollectorFilter, options?: ReactionCollectorOptions) { - super(message.client, filter, options || {}); - this.message = message; - this.users = new Collection(); - this.total = 0; - this.client.on('messageReactionAdd', this.listener); - } - - public handle(reaction: MessageReaction): CollectorHandler { - if (reaction.message.id !== this.message.id) { return null; } - return { - key: reaction.emoji.id || reaction.emoji.name, - value: reaction - }; - } - - public postCheck(reaction: MessageReaction, user: User): string { - this.users.set(user.id, user); - if (this.options.max && ++this.total >= this.options.max) { return 'limit'; } - if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) { return 'emojiLimit'; } - if (this.options.maxUsers && this.users.size >= this.options.maxUsers) { return 'userLimit'; } - return null; - } - - public cleanup(): void { - this.client.removeListener('messageReactionAdd', this.listener); - } -} From 3b91fa0b492d23cb18ed4acc5346a552dd8b2ce7 Mon Sep 17 00:00:00 2001 From: zajrik Date: Sat, 18 Aug 2018 05:40:42 -0500 Subject: [PATCH 0834/1359] tests(lint): Update typings lint script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 169672412..2a189a95d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "docs:test": "docgen --source src --custom docs/index.yml", "lint": "eslint src *.js", "lint:fix": "eslint --fix src", - "lint:typings": "tslint typings/index.d.ts typings/discord.js-test.ts", + "lint:typings": "tslint typings/index.d.ts", "build:browser": "webpack", "prepublishOnly": "npm run test && NODE_ENV=production npm run build:browser" }, From 94214fa73321f7fb73a149403dcb765cdad10372 Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Sat, 18 Aug 2018 14:21:24 +0300 Subject: [PATCH 0835/1359] misc: Remove Clyde (#2764) * Remove Clyde * Remove Clyde avatar overwrite * Remove Clyde from the user pattern --- src/client/websocket/packets/handlers/Ready.js | 13 ------------- src/structures/MessageMentions.js | 2 +- src/util/Constants.js | 1 - 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index c1a16df02..0e16400d1 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -19,19 +19,6 @@ class ReadyHandler extends AbstractHandler { for (const guild of data.guilds) client.guilds.add(guild); for (const privateDM of data.private_channels) client.channels.add(privateDM); - if (!client.users.has('1')) { - client.users.add({ - id: '1', - username: 'Clyde', - discriminator: '0000', - avatar: 'https://discordapp.com/assets/f78426a064bc9dd24847519259bc42af.png', - bot: true, - status: 'online', - activity: null, - verified: true, - }); - } - const t = client.setTimeout(() => { client.ws.connection.triggerReady(); }, 1200 * data.guilds.length); diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 388da1c9f..bd99397d7 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -160,7 +160,7 @@ MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; * Regular expression that globally matches user mentions like `<@81440962496172032>` * @type {RegExp} */ -MessageMentions.USERS_PATTERN = /<@!?(1|\d{17,19})>/g; +MessageMentions.USERS_PATTERN = /<@!?(\d{17,19})>/g; /** * Regular expression that globally matches role mentions like `<@&297577916114403338>` diff --git a/src/util/Constants.js b/src/util/Constants.js index ce5427552..80bc911ce 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -124,7 +124,6 @@ exports.Endpoints = { Asset: name => `${root}/assets/${name}`, DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, Avatar: (userID, hash, format = 'default', size) => { - if (userID === '1') return hash; if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); }, From f8057b01cb321d468c61b04227b9589fb4dcded1 Mon Sep 17 00:00:00 2001 From: Florian Date: Sat, 18 Aug 2018 13:57:58 +0200 Subject: [PATCH 0836/1359] feat: add guild.fetchVanityCode() (#2732) * Error for guild.fetchVanityURL feature not enabled * added GET/guilds/{guild.id}/vanity-url endpoint * fix: code conventions * adopted suggestion * Changed error message according to change request * Renamed method to indicate that only the code is fetched, not the entire url. * Update Guild.js --- src/errors/Messages.js | 2 ++ src/structures/Guild.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 86bd54cd1..f9ff8be6e 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -97,6 +97,8 @@ const Messages = { EMOJI_MANAGED: 'Emoji is managed and has no Author.', REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.', + + VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', }; for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 423083e93..d80620364 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -434,6 +434,26 @@ class Guild extends Base { }); } + /** + * Fetches the vanity url invite code to this guild. + * Resolves with a string matching the vanity url invite code, not the full url. + * @returns {Promise} + * @example + * // Fetch invites + * guild.fetchVanityCode() + * .then(code => { + * console.log(`Vanity URL: https://discord.gg/${code}`); + * }) + * .catch(console.error); + */ + fetchVanityCode() { + if (!this.features.includes('VANITY_URL')) { + return Promise.reject(new Error('VANITY_URL')); + } + return this.client.api.guilds(this.id, 'vanity-url').get() + .then(res => res.code); + } + /** * Fetches all webhooks for the guild. * @returns {Promise>} From 7546ca3fe02fdd0f48097d9cbb3a8ec1284b3bfb Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 18 Aug 2018 17:22:57 +0200 Subject: [PATCH 0837/1359] typings(Guild): add fetchVanityCode PR: #2732 Commit: https://github.com/discordjs/discord.js/commit/f8057b01cb321d468c61b04227b9589fb4dcded1 --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index eb42e6333..a56ef8182 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -390,6 +390,7 @@ declare module 'discord.js' { public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchBans(): Promise>; public fetchInvites(): Promise>; + public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; public iconURL(options?: AvatarOptions): string; From 18d6be1eb37392ffa9e7bac77aec25cb15dc5109 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Sat, 18 Aug 2018 09:35:27 -0700 Subject: [PATCH 0838/1359] update PR template (#2770) * update PR template to encourage testing/typings * add extra linebreak --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 735a552ec..184407860 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,10 @@ **Please describe the changes this PR makes and why it should be merged:** +**Status** +- [ ] Code changes have been tested against the Discord API, or there are no code changes +- [ ] I know how to update typings and have done so, or typings don't need updating + **Semantic versioning classification:** - [ ] This PR changes the library's interface (methods or parameters added) - [ ] This PR includes breaking changes (methods removed or renamed, parameters moved or removed) From 68c832957d690b72cdfa543d162d8f369c8c02d7 Mon Sep 17 00:00:00 2001 From: zajrik Date: Sat, 18 Aug 2018 22:23:13 -0500 Subject: [PATCH 0839/1359] tests: Add typings lint runner to test script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a189a95d..44625fd78 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "./src/index", "types": "./typings/index.d.ts", "scripts": { - "test": "npm run lint && npm run docs:test", + "test": "npm run lint && npm run docs:test && npm run lint:typings", "docs": "docgen --source src --custom docs/index.yml --output docs/docs.json", "docs:test": "docgen --source src --custom docs/index.yml", "lint": "eslint src *.js", From 4a24e8c12c1eeef84b804ea21c5b5d1f0573c24a Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Sun, 19 Aug 2018 14:37:00 +0300 Subject: [PATCH 0840/1359] Remove double iteration (#2773) --- src/structures/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/User.js b/src/structures/User.js index 1fb23d4cf..673663234 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -185,7 +185,7 @@ class User extends Base { * @readonly */ get dmChannel() { - return this.client.channels.filter(c => c.type === 'dm').find(c => c.recipient.id === this.id) || null; + return this.client.channels.find(c => c.type === 'dm' && c.recipient.id === this.id) || null; } /** From f33ad64d12fb111efc7df95c2b78ac61737f04e8 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 20 Aug 2018 18:30:19 +0200 Subject: [PATCH 0841/1359] chore: up ecmaVersion in eslint to support object rest spread --- .eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 376e9f70d..9b7c229b6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": "eslint:recommended", "parserOptions": { - "ecmaVersion": 2017 + "ecmaVersion": 2018 }, "env": { "es6": true, From 8afc1fa545848a409685bbd85595fe0bbe11599a Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 20 Aug 2018 18:38:26 +0200 Subject: [PATCH 0842/1359] fix: pin dev-deps to a higher version to avoid problems with upath on node 10 --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 44625fd78..af7b73c72 100644 --- a/package.json +++ b/package.json @@ -51,16 +51,16 @@ "zlib-sync": "^0.1.0" }, "devDependencies": { - "@types/node": "^10.0.3", + "@types/node": "^10.7.1", "discord.js-docgen": "discordjs/docgen", - "eslint": "^5.0.1", + "eslint": "^5.4.0", "json-filter-loader": "^1.0.0", "tslint": "^5.11.0", "tslint-config-typings": "^0.3.1", "typescript": "^3.0.1", - "uglifyjs-webpack-plugin": "^1.1.8", - "webpack": "^4.5.0", - "webpack-cli": "^3.0.1" + "uglifyjs-webpack-plugin": "^1.3.0", + "webpack": "^4.16.5", + "webpack-cli": "^3.1.0" }, "engines": { "node": ">=8.0.0" From ab0ede0d5a8dcb4f02a998123d7347be00a38e48 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 20 Aug 2018 18:43:53 +0200 Subject: [PATCH 0843/1359] fix: pin peer-deps to the latest version we allow --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index af7b73c72..9f4e7fec2 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,12 @@ "ws": "^6.0.0" }, "peerDependencies": { - "bufferutil": "^3.0.0", + "bufferutil": "^4.0.0", "erlpack": "discordapp/erlpack", - "libsodium-wrappers": "^0.7.0", - "sodium": "^2.0.0", - "uws": "^9.14.0", - "zlib-sync": "^0.1.0" + "libsodium-wrappers": "^0.7.3", + "sodium": "^2.0.3", + "uws": "<=10.148.1", + "zlib-sync": "^0.1.4" }, "devDependencies": { "@types/node": "^10.7.1", From 0401b8ad779a210358975d2e4bd37bd570428c83 Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Tue, 21 Aug 2018 11:38:35 +0300 Subject: [PATCH 0844/1359] feat: handle and forward WEBHOOKS_UPDATE events (#2762) * src: Handle WEBHOOK_UPDATE events * Commit rename of 77' Or adding the letter S * I missed this * Properly do this now Typos everywhere * Typings * refactor: remove now unnecessary guild variable --- .../packets/WebSocketPacketManager.js | 1 + .../packets/handlers/WebhooksUpdate.js | 19 +++++++++++++++++++ src/util/Constants.js | 3 +++ typings/index.d.ts | 7 +++++-- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/client/websocket/packets/handlers/WebhooksUpdate.js diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js index f9d7b13ae..06fcae12f 100644 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ b/src/client/websocket/packets/WebSocketPacketManager.js @@ -47,6 +47,7 @@ class WebSocketPacketManager { this.register(WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd')); this.register(WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove')); this.register(WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll')); + this.register(WSEvents.WEBHOOKS_UPDATE, require('./handlers/WebhooksUpdate')); } get client() { diff --git a/src/client/websocket/packets/handlers/WebhooksUpdate.js b/src/client/websocket/packets/handlers/WebhooksUpdate.js new file mode 100644 index 000000000..7ed2721e3 --- /dev/null +++ b/src/client/websocket/packets/handlers/WebhooksUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const { Events } = require('../../../../util/Constants'); + +class WebhooksUpdate extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const channel = client.channels.get(data.channel_id); + if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); + } +} + +/** + * Emitted whenever a guild text channel has its webhooks changed. + * @event Client#webhookUpdate + * @param {TextChannel} channel The channel that had a webhook update + */ + +module.exports = WebhooksUpdate; diff --git a/src/util/Constants.js b/src/util/Constants.js index 80bc911ce..ed012d8c8 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -249,6 +249,7 @@ exports.Events = { VOICE_BROADCAST_UNSUBSCRIBE: 'unsubscribe', TYPING_START: 'typingStart', TYPING_STOP: 'typingStop', + WEBHOOKS_UPDATE: 'webhookUpdate', DISCONNECT: 'disconnect', RECONNECTING: 'reconnecting', ERROR: 'error', @@ -290,6 +291,7 @@ exports.Events = { * * VOICE_STATE_UPDATE * * TYPING_START * * VOICE_SERVER_UPDATE + * * WEBHOOKS_UPDATE * @typedef {string} WSEventType */ exports.WSEvents = keyMirror([ @@ -324,6 +326,7 @@ exports.WSEvents = keyMirror([ 'VOICE_STATE_UPDATE', 'TYPING_START', 'VOICE_SERVER_UPDATE', + 'WEBHOOKS_UPDATE', ]); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index a56ef8182..6bd46c7fb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -140,7 +140,8 @@ declare module 'discord.js' { public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; - public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; + public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; + public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -171,6 +172,7 @@ declare module 'discord.js' { public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; + public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: string, listener: Function): this; } @@ -1976,7 +1978,8 @@ declare module 'discord.js' { | 'PRESENCE_UPDATE' | 'VOICE_STATE_UPDATE' | 'TYPING_START' - | 'VOICE_SERVER_UPDATE'; + | 'VOICE_SERVER_UPDATE' + | 'WEBHOOKS_UPDATE'; //#endregion } From 5787deef264fd5cc2179c5f9584d49443d1eb0d9 Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 21 Aug 2018 10:40:47 +0200 Subject: [PATCH 0845/1359] feat: GuildEmbed support (#2766) * feat: Guild embed support * docs: Fixed setEmbed's reason argument not being optional * fix: Guild#setEmbed should return the guild itself for consistency * docs: Updated typings * fix: Requested change --- src/structures/Guild.js | 39 +++++++++++++++++++++++++++++++++++++++ typings/index.d.ts | 7 +++++++ 2 files changed, 46 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index d80620364..a698c1049 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -483,6 +483,29 @@ class Guild extends Base { }); } + /** + * The Guild Embed object + * @typedef {Object} GuildEmbedData + * @property {boolean} enabled Whether the embed is enabled + * @property {?GuildChannel} channel The embed channel + */ + + /** + * Fetches the guild embed. + * @returns {Promise} + * @example + * // Fetches the guild embed + * guild.fetchEmbed() + * .then(embed => console.log(`The embed is ${embed.enabled ? 'enabled' : 'disabled'}`)) + * .catch(console.error); + */ + fetchEmbed() { + return this.client.api.guilds(this.id).embed.get().then(data => ({ + enabled: data.enabled, + channel: data.channel_id ? this.channels.get(data.channel_id) : null, + })); + } + /** * Fetches audit logs for this guild. * @param {Object} [options={}] Options for fetching audit logs @@ -790,6 +813,22 @@ class Guild extends Base { ); } + /** + * Edits the guild's embed. + * @param {GuildEmbedData} embed The embed for the guild + * @param {string} [reason] Reason for changing the guild's embed + * @returns {Promise} + */ + setEmbed(embed, reason) { + return this.client.api.guilds(this.id).embed.patch({ + data: { + enabled: embed.enabled, + channel_id: this.channels.resolveID(embed.channel), + }, + reason, + }).then(() => this); + } + /** * Leaves the guild. * @returns {Promise} diff --git a/typings/index.d.ts b/typings/index.d.ts index 6bd46c7fb..e4887937e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -395,6 +395,7 @@ declare module 'discord.js' { public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; + public fetchEmbed(): Promise; public iconURL(options?: AvatarOptions): string; public leave(): Promise; public member(user: UserResolvable): GuildMember; @@ -410,6 +411,7 @@ declare module 'discord.js' { public setSplash(splash: Base64Resolvable, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; + public setEmbed(embed: GuildEmbedData, reason?: string): Promise; public splashURL(options?: AvatarOptions): string; public toJSON(): object; public toString(): string; @@ -1698,6 +1700,11 @@ declare module 'discord.js' { splash?: Base64Resolvable; }; + type GuildEmbedData = { + enabled: boolean; + channel?: GuildChannelResolvable; + }; + type GuildFeatures = 'INVITE_SPLASH' | 'MORE_EMOJI' | 'VERIFIED' From 6be8172539065675c1cd7c12f25ffbaa19f66713 Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 21 Aug 2018 10:41:42 +0200 Subject: [PATCH 0846/1359] misc: add `UNKNOWN_WEBHOOK` error code (#2775) * Add UNKNOWN_WEBHOOK error code * docs: Update typings --- src/util/Constants.js | 2 ++ typings/index.d.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index ed012d8c8..69ad7620f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -435,6 +435,7 @@ exports.Colors = { * * UNKNOWN_TOKEN * * UNKNOWN_USER * * UNKNOWN_EMOJI + * * UNKNOWN_WEBHOOK * * BOT_PROHIBITED_ENDPOINT * * BOT_ONLY_ENDPOINT * * MAXIMUM_GUILDS @@ -481,6 +482,7 @@ exports.APIErrors = { UNKNOWN_TOKEN: 10012, UNKNOWN_USER: 10013, UNKNOWN_EMOJI: 10014, + UNKNOWN_WEBHOOK: 10015, BOT_PROHIBITED_ENDPOINT: 20001, BOT_ONLY_ENDPOINT: 20002, MAXIMUM_GUILDS: 30001, diff --git a/typings/index.d.ts b/typings/index.d.ts index e4887937e..b678c5a2c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1380,6 +1380,7 @@ declare module 'discord.js' { UNKNOWN_TOKEN: number; UNKNOWN_USER: number; UNKNOWN_EMOJI: number; + UNKNOWN_WEBHOOK: number; BOT_PROHIBITED_ENDPOINT: number; BOT_ONLY_ENDPOINT: number; MAXIMUM_GUILDS: number; From c62f01f0e4e3c24c3227b10704ac3ccf8744a6e1 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 21 Aug 2018 04:56:41 -0500 Subject: [PATCH 0847/1359] refactor(BitField): base class for Permissions, ActivityFlags, Speaking (#2765) * abstract BitField from Permissions * reduce useless code, improve docs * add a ReadOnly identifier to the return type of Bitfield#freeze() https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#partial-readonly-record-and-pick * fix the RangeError * update docs, convert Speaking and ActivityFlags to bitfields * fix some docs * Fix Speaking BitField oops * docs for oops * more incorrect docs * Fix incorrectly named property * add new classes to index * fix missing @extends docs * default bitfield resolve to 0, and cleanup defaulting everywhere Also removes GuildMember#missiongPermissions() alias that had incorrect behavior * Breaking: Rename Overwrite allowed and denied to allow and deny To be consistent with the api's naming * fix setSpeaking usage to bitfields instead of booleans * fix speaking bug in playChunk * docs: Updated typings * fix: BitFieldResolvable should use RecursiveArray * bugfix/requested change * typings: Cleanup (#2) * typings: Fix BitField#{toArray,@@iterator} output type * typings: correct PermissionOverwrites property names and nitpicks --- src/client/Client.js | 2 +- src/client/voice/VoiceConnection.js | 35 ++-- .../voice/dispatcher/StreamDispatcher.js | 9 +- src/errors/Messages.js | 2 +- src/index.js | 3 + src/stores/GuildChannelStore.js | 6 +- src/structures/GuildChannel.js | 54 +++---- src/structures/GuildMember.js | 14 +- src/structures/PermissionOverwrites.js | 8 +- src/structures/Presence.js | 16 +- src/structures/Role.js | 4 +- src/structures/shared/resolvePermissions.js | 4 +- src/util/ActivityFlags.js | 29 ++++ src/util/BitField.js | 151 ++++++++++++++++++ src/util/Constants.js | 9 -- src/util/Permissions.js | 130 ++------------- src/util/Speaking.js | 22 +++ typings/index.d.ts | 83 ++++++---- 18 files changed, 339 insertions(+), 242 deletions(-) create mode 100644 src/util/ActivityFlags.js create mode 100644 src/util/BitField.js create mode 100644 src/util/Speaking.js diff --git a/src/client/Client.js b/src/client/Client.js index 49a91f389..f3a6617d5 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -355,7 +355,7 @@ class Client extends BaseClient { * .catch(console.error); */ generateInvite(permissions) { - permissions = typeof permissions === 'undefined' ? 0 : Permissions.resolve(permissions); + permissions = Permissions.resolve(permissions); return this.fetchApplication().then(application => `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot` ); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6682d32ff..eeaf768d0 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -7,6 +7,7 @@ const VoiceReceiver = require('./receiver/Receiver'); const EventEmitter = require('events'); const { Error } = require('../../errors'); const PlayInterface = require('./util/PlayInterface'); +const Speaking = require('../../util/Speaking'); const SUPPORTED_MODES = [ 'xsalsa20_poly1305_lite', @@ -49,10 +50,10 @@ class VoiceConnection extends EventEmitter { this.status = VoiceStatus.AUTHENTICATING; /** - * Whether we're currently transmitting audio - * @type {boolean} + * Our current speaking state + * @type {ReadOnly} */ - this.speaking = false; + this.speaking = new Speaking().freeze(); /** * The authentication data needed to connect to the voice server @@ -96,7 +97,7 @@ class VoiceConnection extends EventEmitter { /** * Tracks which users are talking - * @type {Map} + * @type {Map>} * @private */ this._speaking = new Map(); @@ -135,18 +136,18 @@ class VoiceConnection extends EventEmitter { } /** - * Sets whether the voice connection should display as "speaking" or not. - * @param {boolean} value Whether or not to speak + * Sets whether the voice connection should display as "speaking", "soundshare" or "none". + * @param {BitFieldResolvable} value The new speaking state * @private */ setSpeaking(value) { - if (this.speaking === value) return; + if (this.speaking.equals(value)) return; if (this.status !== VoiceStatus.CONNECTED) return; - this.speaking = value; + this.speaking = new Speaking(value).freeze(); this.sockets.ws.sendPacket({ op: VoiceOPCodes.SPEAKING, d: { - speaking: this.speaking ? 1 : 0, + speaking: this.speaking.bitfield, delay: 0, ssrc: this.authentication.ssrc, }, @@ -305,7 +306,7 @@ class VoiceConnection extends EventEmitter { reconnect(token, endpoint) { this.authentication.token = token; this.authentication.endpoint = endpoint; - this.speaking = false; + this.speaking = new Speaking().freeze(); this.status = VoiceStatus.RECONNECTING; /** * Emitted when the voice connection is reconnecting (typically after a region change). @@ -350,7 +351,7 @@ class VoiceConnection extends EventEmitter { */ cleanup() { this.player.destroy(); - this.speaking = false; + this.speaking = new Speaking().freeze(); const { ws, udp } = this.sockets; if (ws) { @@ -432,17 +433,17 @@ class VoiceConnection extends EventEmitter { * @private */ onSpeaking({ user_id, ssrc, speaking }) { - speaking = Boolean(speaking); + speaking = new Speaking(speaking).freeze(); const guild = this.channel.guild; const user = this.client.users.get(user_id); this.ssrcMap.set(+ssrc, user_id); const old = this._speaking.get(user_id); this._speaking.set(user_id, speaking); /** - * Emitted whenever a user starts/stops speaking. + * Emitted whenever a user changes speaking state. * @event VoiceConnection#speaking - * @param {User} user The user that has started/stopped speaking - * @param {boolean} speaking Whether or not the user is speaking + * @param {User} user The user that has changed speaking state + * @param {ReadOnly} speaking The speaking state of the user */ if (this.status === VoiceStatus.CONNECTED) { this.emit('speaking', user, speaking); @@ -455,10 +456,10 @@ class VoiceConnection extends EventEmitter { const member = guild.member(user); if (member) { /** - * Emitted once a guild member starts/stops speaking. + * Emitted once a guild member changes speaking state. * @event Client#guildMemberSpeaking * @param {GuildMember} member The member that started/stopped speaking - * @param {boolean} speaking Whether or not the member is speaking + * @param {ReadOnly} speaking The speaking state of the member */ this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); } diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index e640b1556..7bb0ef1b0 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -67,7 +67,7 @@ class StreamDispatcher extends Writable { this.on('finish', () => { // Still emitting end for backwards compatibility, probably remove it in the future! this.emit('end'); - this._setSpeaking(false); + this._setSpeaking(0); }); if (typeof volume !== 'undefined') this.setVolume(volume); @@ -131,7 +131,7 @@ class StreamDispatcher extends Writable { this.streams.silence.pipe(this); this._silence = true; } else { - this._setSpeaking(false); + this._setSpeaking(0); } this.pausedSince = Date.now(); } @@ -243,7 +243,6 @@ class StreamDispatcher extends Writable { _playChunk(chunk) { if (this.player.dispatcher !== this || !this.player.voiceConnection.authentication.secret_key) return; - this._setSpeaking(true); this._sendPacket(this._createPacket(this._sdata.sequence, this._sdata.timestamp, chunk)); } @@ -285,7 +284,7 @@ class StreamDispatcher extends Writable { * @event StreamDispatcher#debug * @param {string} info The debug info */ - this._setSpeaking(true); + this._setSpeaking(1); while (repeats--) { if (!this.player.voiceConnection.sockets.udp) { this.emit('debug', 'Failed to send a packet - no UDP socket'); @@ -293,7 +292,7 @@ class StreamDispatcher extends Writable { } this.player.voiceConnection.sockets.udp.send(packet) .catch(e => { - this._setSpeaking(false); + this._setSpeaking(0); this.emit('debug', `Failed to send a packet - ${e}`); }); } diff --git a/src/errors/Messages.js b/src/errors/Messages.js index f9ff8be6e..00f99a676 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -10,7 +10,7 @@ const Messages = { WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, - PERMISSIONS_INVALID: 'Invalid permission string or number.', + BITFIELD_INVALID: 'Invalid bitfield flag or number.', RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.', diff --git a/src/index.js b/src/index.js index 4bb024e31..fd2239ade 100644 --- a/src/index.js +++ b/src/index.js @@ -10,12 +10,15 @@ module.exports = { WebhookClient: require('./client/WebhookClient'), // Utilities + ActivityFlags: require('./util/ActivityFlags'), + BitField: require('./util/BitField'), Collection: require('./util/Collection'), Constants: require('./util/Constants'), DataResolver: require('./util/DataResolver'), DataStore: require('./stores/DataStore'), DiscordAPIError: require('./rest/DiscordAPIError'), Permissions: require('./util/Permissions'), + Speaking: require('./util/Speaking'), Snowflake: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'), Structures: require('./util/Structures'), diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 03efe2315..e73b52e98 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -24,8 +24,8 @@ class GuildChannelStore extends DataStore { /** * Can be used to overwrite permissions when creating a channel. * @typedef {Object} PermissionOverwriteOptions - * @property {PermissionResolvable} [allowed] The permissions to allow - * @property {PermissionResolvable} [denied] The permissions to deny + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny * @property {RoleResolvable|UserResolvable} id ID of the role or member this overwrite is for */ @@ -54,7 +54,7 @@ class GuildChannelStore extends DataStore { * overwrites: [ * { * id: message.author.id, - * denied: ['VIEW_CHANNEL'], + * deny: ['VIEW_CHANNEL'], * }, * ], * }) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 9467371f9..b64eca976 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -76,8 +76,8 @@ class GuildChannel extends Channel { return !this.permissionOverwrites.find((value, key) => { const testVal = this.parent.permissionOverwrites.get(key); return testVal === undefined || - testVal.denied.bitfield !== value.denied.bitfield || - testVal.allowed.bitfield !== value.allowed.bitfield; + testVal.deny.bitfield !== value.deny.bitfield || + testVal.allow.bitfield !== value.allow.bitfield; }); } @@ -133,7 +133,7 @@ class GuildChannel extends Channel { /** * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites. * @param {GuildMember} member The member to obtain the overall permissions for - * @returns {Permissions} + * @returns {ReadOnly} * @private */ memberPermissions(member) { @@ -147,19 +147,19 @@ class GuildChannel extends Channel { const overwrites = this.overwritesFor(member, true, roles); return permissions - .remove(overwrites.everyone ? overwrites.everyone.denied : 0) - .add(overwrites.everyone ? overwrites.everyone.allowed : 0) - .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.denied) : 0) - .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allowed) : 0) - .remove(overwrites.member ? overwrites.member.denied : 0) - .add(overwrites.member ? overwrites.member.allowed : 0) + .remove(overwrites.everyone ? overwrites.everyone.deny : 0) + .add(overwrites.everyone ? overwrites.everyone.allow : 0) + .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : 0) + .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : 0) + .remove(overwrites.member ? overwrites.member.deny : 0) + .add(overwrites.member ? overwrites.member.allow : 0) .freeze(); } /** * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. * @param {Role} role The role to obtain the overall permissions for - * @returns {Permissions} + * @returns {ReadOnly} * @private */ rolePermissions(role) { @@ -169,10 +169,10 @@ class GuildChannel extends Channel { const roleOverwrites = this.permissionOverwrites.get(role.id); return role.permissions - .remove(everyoneOverwrites ? everyoneOverwrites.denied : 0) - .add(everyoneOverwrites ? everyoneOverwrites.allowed : 0) - .remove(roleOverwrites ? roleOverwrites.denied : 0) - .add(roleOverwrites ? roleOverwrites.allowed : 0) + .remove(everyoneOverwrites ? everyoneOverwrites.deny : 0) + .add(everyoneOverwrites ? everyoneOverwrites.allow : 0) + .remove(roleOverwrites ? roleOverwrites.deny : 0) + .add(roleOverwrites ? roleOverwrites.allow : 0) .freeze(); } @@ -188,7 +188,7 @@ class GuildChannel extends Channel { * overwrites: [ * { * id: message.author.id, - * denied: ['VIEW_CHANNEL'], + * deny: ['VIEW_CHANNEL'], * }, * ], * reason: 'Needed to change permissions' @@ -227,8 +227,8 @@ class GuildChannel extends Channel { * .catch(console.error); */ updateOverwrite(userOrRole, options, reason) { - const allow = new Permissions(0); - const deny = new Permissions(0); + const allow = new Permissions(); + const deny = new Permissions(); let type; const role = this.guild.roles.get(userOrRole); @@ -245,20 +245,20 @@ class GuildChannel extends Channel { const prevOverwrite = this.permissionOverwrites.get(userOrRole.id); if (prevOverwrite) { - allow.add(prevOverwrite.allowed); - deny.add(prevOverwrite.denied); + allow.add(prevOverwrite.allow); + deny.add(prevOverwrite.deny); } for (const perm in options) { if (options[perm] === true) { - allow.add(Permissions.FLAGS[perm] || 0); - deny.remove(Permissions.FLAGS[perm] || 0); + allow.add(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); } else if (options[perm] === false) { - allow.remove(Permissions.FLAGS[perm] || 0); - deny.add(Permissions.FLAGS[perm] || 0); + allow.remove(Permissions.FLAGS[perm]); + deny.add(Permissions.FLAGS[perm]); } else if (options[perm] === null) { - allow.remove(Permissions.FLAGS[perm] || 0); - deny.remove(Permissions.FLAGS[perm] || 0); + allow.remove(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); } } @@ -274,8 +274,8 @@ class GuildChannel extends Channel { lockPermissions() { if (!this.parent) return Promise.reject(new Error('GUILD_CHANNEL_ORPHAN')); const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => ({ - deny: overwrite.denied.bitfield, - allow: overwrite.allowed.bitfield, + deny: overwrite.deny.bitfield, + allow: overwrite.allow.bitfield, id: overwrite.id, type: overwrite.type, })); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index fccfcdd4a..de42e354c 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -168,7 +168,7 @@ class GuildMember extends Base { /** * The overall set of permissions for this member, taking only roles into account - * @type {Permissions} + * @type {ReadOnly} * @readonly */ get permissions() { @@ -209,7 +209,7 @@ class GuildMember extends Base { * Returns `channel.permissionsFor(guildMember)`. Returns permissions for a member in a guild channel, * taking into account roles and permission overwrites. * @param {ChannelResolvable} channel The guild channel to use as context - * @returns {?Permissions} + * @returns {ReadOnly} */ permissionsIn(channel) { channel = this.guild.channels.resolve(channel); @@ -230,16 +230,6 @@ class GuildMember extends Base { return this.roles.some(r => r.permissions.has(permission, checkAdmin)); } - /** - * Checks whether the roles of this member allows them to perform specific actions, and lists any missing permissions. - * @param {PermissionResolvable} permissions The permissions to check for - * @param {boolean} [explicit=false] Whether to require the member to explicitly have the exact permissions - * @returns {PermissionResolvable[]} - */ - missingPermissions(permissions, explicit = false) { - return this.permissions.missing(permissions, explicit); - } - /** * The data for editing a guild member. * @typedef {Object} GuildMemberEditData diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 5533a95a8..f2ece7ced 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -39,15 +39,15 @@ class PermissionOverwrites { /** * The permissions that are denied for the user or role. - * @type {Permissions} + * @type {ReadOnly} */ - this.denied = new Permissions(data.deny).freeze(); + this.deny = new Permissions(data.deny).freeze(); /** * The permissions that are allowed for the user or role. - * @type {Permissions} + * @type {ReadOnly} */ - this.allowed = new Permissions(data.allow).freeze(); + this.allow = new Permissions(data.allow).freeze(); } /** diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 3ade96bf7..00cdaa051 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,5 +1,6 @@ const Util = require('../util/Util'); -const { ActivityTypes, ActivityFlags } = require('../util/Constants'); +const ActivityFlags = require('../util/ActivityFlags'); +const { ActivityTypes } = require('../util/Constants'); /** * Activity sent in a message. @@ -150,15 +151,12 @@ class Activity { this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null; this.syncID = data.sync_id; - this._flags = data.flags; - } - get flags() { - const flags = []; - for (const [name, flag] of Object.entries(ActivityFlags)) { - if ((this._flags & flag) === flag) flags.push(name); - } - return flags; + /** + * Flags that describe the activity + * @type {ReadoOnly} + */ + this.flags = new ActivityFlags(data.flags).freeze(); } /** diff --git a/src/structures/Role.js b/src/structures/Role.js index 43d74c15d..e6808a822 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -54,7 +54,7 @@ class Role extends Base { /** * The permissions of the role - * @type {Permissions} + * @type {ReadOnly} */ this.permissions = new Permissions(data.permissions).freeze(); @@ -203,7 +203,7 @@ class Role extends Base { * Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel, * taking into account permission overwrites. * @param {ChannelResolvable} channel The guild channel to use as context - * @returns {?Permissions} + * @returns {ReadOnly} */ permissionsIn(channel) { channel = this.guild.channels.resolve(channel); diff --git a/src/structures/shared/resolvePermissions.js b/src/structures/shared/resolvePermissions.js index d742b63fb..c06f83e0f 100644 --- a/src/structures/shared/resolvePermissions.js +++ b/src/structures/shared/resolvePermissions.js @@ -14,8 +14,8 @@ module.exports = function resolvePermissions(overwrites) { } return { - allow: Permissions.resolve(overwrite.allowed || 0), - deny: Permissions.resolve(overwrite.denied || 0), + allow: Permissions.resolve(overwrite.allow), + deny: Permissions.resolve(overwrite.deny), type: overwrite.type, id: overwrite.id, }; diff --git a/src/util/ActivityFlags.js b/src/util/ActivityFlags.js new file mode 100644 index 000000000..49fd7a678 --- /dev/null +++ b/src/util/ActivityFlags.js @@ -0,0 +1,29 @@ +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link Activity#flags} bitfield. + * @extends {BitField} + */ +class ActivityFlags extends BitField {} + +/** + * Numeric activity flags. All available properties: + * * `INSTANCE` + * * `JOIN` + * * `SPECTATE` + * * `JOIN_REQUEST` + * * `SYNC` + * * `PLAY` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/topics/gateway#activity-object-activity-flags} + */ +ActivityFlags.FLAGS = { + INSTANCE: 1 << 0, + JOIN: 1 << 1, + SPECTATE: 1 << 2, + JOIN_REQUEST: 1 << 3, + SYNC: 1 << 4, + PLAY: 1 << 5, +}; + +module.exports = ActivityFlags; diff --git a/src/util/BitField.js b/src/util/BitField.js new file mode 100644 index 000000000..2cd9c907a --- /dev/null +++ b/src/util/BitField.js @@ -0,0 +1,151 @@ +const { RangeError } = require('../errors'); + +/** + * Data structure that makes it easy to interact with a bitfield. + */ +class BitField { + /** + * @param {BitFieldResolvable} [bits=0] Bits(s) to read from + */ + constructor(bits) { + /** + * Bitfield of the packed bits + * @type {number} + */ + this.bitfield = this.constructor.resolve(bits); + } + + /** + * Checks if this bitfield equals another + * @param {BitFieldResolvable} bit Bit(s) to check for + * @returns {boolean} + */ + equals(bit) { + return this.bitfield === this.constructor.resolve(bit); + } + + /** + * Checks whether the bitfield has a bit, or multiple bits. + * @param {BitFieldResolvable} bit Bit(s) to check for + * @returns {boolean} + */ + has(bit) { + if (bit instanceof Array) return bit.every(p => this.has(p)); + bit = this.constructor.resolve(bit); + return (this.bitfield & bit) === bit; + } + + /** + * Gets all given bits that are missing from the bitfield. + * @param {BitFieldResolvable} bits Bits(s) to check for + * @param {...*} hasParams Additional parameters for the has method, if any + * @returns {string[]} + */ + missing(bits, ...hasParams) { + if (!(bits instanceof Array)) bits = new this.constructor(bits).toArray(false); + return bits.filter(p => !this.has(p, ...hasParams)); + } + + /** + * Freezes these bits, making them immutable. + * @returns {ReadOnly} These bits + */ + freeze() { + return Object.freeze(this); + } + + /** + * Adds bits to these ones. + * @param {...BitFieldResolvable} [bits] Bits to add + * @returns {BitField} These bits or new BitField if the instance is frozen. + */ + add(...bits) { + let total = 0; + for (const bit of bits) { + total |= this.constructor.resolve(bit); + } + if (Object.isFrozen(this)) return new this.constructor(this.bitfield | total); + this.bitfield |= total; + return this; + } + + /** + * Removes bits from these. + * @param {...BitFieldResolvable} [bits] Bits to remove + * @returns {BitField} These bits or new BitField if the instance is frozen. + */ + remove(...bits) { + let total = 0; + for (const bit of bits) { + total |= this.constructor.resolve(bit); + } + if (Object.isFrozen(this)) return new this.constructor(this.bitfield & ~total); + this.bitfield &= ~total; + return this; + } + + /** + * Gets an object mapping field names to a {@link boolean} indicating whether the + * bit is available. + * @param {...*} hasParams Additional parameters for the has method, if any + * @returns {Object} + */ + serialize(...hasParams) { + const serialized = {}; + for (const perm in this.constructor.FLAGS) serialized[perm] = this.has(perm, ...hasParams); + return serialized; + } + + /** + * Gets an {@link Array} of bitfield names based on the bits available. + * @param {...*} hasParams Additional parameters for the has method, if any + * @returns {string[]} + */ + toArray(...hasParams) { + return Object.keys(this.constructor.FLAGS).filter(bit => this.has(bit, ...hasParams)); + } + + toJSON() { + return this.bitfield; + } + + valueOf() { + return this.bitfield; + } + + *[Symbol.iterator]() { + yield* this.toArray(); + } + + /** + * Data that can be resolved to give a bitfield. This can be: + * * A string (see {@link BitField.FLAGS}) + * * A bit number + * * An instance of BitField + * * An Array of BitFieldResolvable + * @typedef {string|number|BitField|BitFieldResolvable[]} BitFieldResolvable + */ + + /** + * Resolves bitfields to their numeric form. + * @param {BitFieldResolvable} [bit=0] - bit(s) to resolve + * @returns {number} + */ + static resolve(bit = 0) { + if (typeof bit === 'number' && bit >= 0) return bit; + if (bit instanceof BitField) return bit.bitfield; + if (bit instanceof Array) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); + if (typeof bit === 'string') return this.FLAGS[bit]; + throw new RangeError('BITFIELD_INVALID'); + } +} + +/** + * Numeric bitfield flags. + * Defined in extension classes + * @type {Object} + * @abstract + */ +BitField.FLAGS = {}; + +module.exports = BitField; diff --git a/src/util/Constants.js b/src/util/Constants.js index 69ad7620f..3e1b317d4 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -367,15 +367,6 @@ exports.ActivityTypes = [ 'WATCHING', ]; -exports.ActivityFlags = { - INSTANCE: 1 << 0, - JOIN: 1 << 1, - SPECTATE: 1 << 2, - JOIN_REQUEST: 1 << 3, - SYNC: 1 << 4, - PLAY: 1 << 5, -}; - exports.ChannelTypes = { TEXT: 0, DM: 1, diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 2f56b816f..295346b16 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -1,120 +1,12 @@ -const { RangeError } = require('../errors'); +const BitField = require('./BitField'); /** * Data structure that makes it easy to interact with a permission bitfield. All {@link GuildMember}s have a set of * permissions in their guild, and each channel in the guild may also have {@link PermissionOverwrites} for the member * that override their default permissions. + * @extends {BitField} */ -class Permissions { - /** - * @param {PermissionResolvable} permissions Permission(s) to read from - */ - constructor(permissions) { - /** - * Bitfield of the packed permissions - * @type {number} - */ - this.bitfield = this.constructor.resolve(permissions); - } - - /** - * Checks whether the bitfield has a permission, or multiple permissions. - * @param {PermissionResolvable} permission Permission(s) to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {boolean} - */ - has(permission, checkAdmin = true) { - if (permission instanceof Array) return permission.every(p => this.has(p, checkAdmin)); - permission = this.constructor.resolve(permission); - if (checkAdmin && (this.bitfield & this.constructor.FLAGS.ADMINISTRATOR) > 0) return true; - return (this.bitfield & permission) === permission; - } - - /** - * Gets all given permissions that are missing from the bitfield. - * @param {PermissionResolvable} permissions Permission(s) to check for - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {string[]} - */ - missing(permissions, checkAdmin = true) { - if (!(permissions instanceof Array)) permissions = new this.constructor(permissions).toArray(false); - return permissions.filter(p => !this.has(p, checkAdmin)); - } - - /** - * Freezes these permissions, making them immutable. - * @returns {Permissions} These permissions - */ - freeze() { - return Object.freeze(this); - } - - /** - * Adds permissions to these ones. - * @param {...PermissionResolvable} permissions Permissions to add - * @returns {Permissions} These permissions or new permissions if the instance is frozen. - */ - add(...permissions) { - let total = 0; - for (let p = permissions.length - 1; p >= 0; p--) { - const perm = this.constructor.resolve(permissions[p]); - total |= perm; - } - if (Object.isFrozen(this)) return new this.constructor(this.bitfield | total); - this.bitfield |= total; - return this; - } - - /** - * Removes permissions from these. - * @param {...PermissionResolvable} permissions Permissions to remove - * @returns {Permissions} These permissions or new permissions if the instance is frozen. - */ - remove(...permissions) { - let total = 0; - for (let p = permissions.length - 1; p >= 0; p--) { - const perm = this.constructor.resolve(permissions[p]); - total |= perm; - } - if (Object.isFrozen(this)) return new this.constructor(this.bitfield & ~total); - this.bitfield &= ~total; - return this; - } - - /** - * Gets an object mapping permission name (like `VIEW_CHANNEL`) to a {@link boolean} indicating whether the - * permission is available. - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {Object} - */ - serialize(checkAdmin = true) { - const serialized = {}; - for (const perm in this.constructor.FLAGS) serialized[perm] = this.has(perm, checkAdmin); - return serialized; - } - - /** - * Gets an {@link Array} of permission names (such as `VIEW_CHANNEL`) based on the permissions available. - * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override - * @returns {string[]} - */ - toArray(checkAdmin = true) { - return Object.keys(this.constructor.FLAGS).filter(perm => this.has(perm, checkAdmin)); - } - - toJSON() { - return this.bitfield; - } - - valueOf() { - return this.bitfield; - } - - *[Symbol.iterator]() { - const keys = this.toArray(); - while (keys.length) yield keys.shift(); - } - +class Permissions extends BitField { /** * Data that can be resolved to give a permission number. This can be: * * A string (see {@link Permissions.FLAGS}) @@ -125,16 +17,14 @@ class Permissions { */ /** - * Resolves permissions to their numeric form. - * @param {PermissionResolvable} permission - Permission(s) to resolve - * @returns {number} + * Checks whether the bitfield has a permission, or multiple permissions. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {boolean} */ - static resolve(permission) { - if (typeof permission === 'number' && permission >= 0) return permission; - if (permission instanceof Permissions) return permission.bitfield; - if (permission instanceof Array) return permission.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); - if (typeof permission === 'string') return this.FLAGS[permission]; - throw new RangeError('PERMISSIONS_INVALID'); + has(permission, checkAdmin = true) { + if (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) return true; + return super.has(permission); } } diff --git a/src/util/Speaking.js b/src/util/Speaking.js new file mode 100644 index 000000000..e0e557eaa --- /dev/null +++ b/src/util/Speaking.js @@ -0,0 +1,22 @@ +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link VoiceConnection#speaking} + * and {@link guildMemberSpeaking} event bitfields. + * @extends {BitField} + */ +class Speaking extends BitField {} + +/** + * Numeric speaking flags. All available properties: + * * `SPEAKING` + * * `SOUNDSHARE` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/topics/voice-connections#speaking} + */ +Speaking.FLAGS = { + SPEAKING: 1 << 0, + SOUNDSHARE: 1 << 1, +}; + +module.exports = Speaking; diff --git a/typings/index.d.ts b/typings/index.d.ts index b678c5a2c..67fd0f459 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -35,6 +35,11 @@ declare module 'discord.js' { public equals(activity: Activity): boolean; } + export class ActivityFlags extends BitField { + public static resolve(permission: BitFieldResolvable): number; + public static FLAGS: Record; + } + export class Base { constructor (client: Client); public readonly client: Client; @@ -62,6 +67,24 @@ declare module 'discord.js' { public broadcast: VoiceBroadcast; } + export class BitField { + constructor(bits?: BitFieldResolvable); + public bitfield: number; + public add(...bits: BitFieldResolvable[]): BitField; + public equals(bit: BitFieldResolvable): boolean; + public freeze(): Readonly>; + public has(bit: BitFieldResolvable): boolean; + public missing(bits: BitFieldResolvable, ...hasParams: any[]): S[]; + public remove(...bits: BitFieldResolvable[]): BitField; + public serialize(...hasParams: BitFieldResolvable[]): Record; + public toArray(): S[]; + public toJSON(): number; + public valueOf(): number; + public [Symbol.iterator](): Iterator; + public static resolve(bit?: BitFieldResolvable): number; + public static FLAGS: { [key: string]: number }; + } + export class CategoryChannel extends GuildChannel { public readonly children: Collection; } @@ -125,7 +148,7 @@ declare module 'discord.js' { public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; @@ -156,7 +179,7 @@ declare module 'discord.js' { public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: boolean) => void): this; + public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; @@ -450,8 +473,8 @@ declare module 'discord.js' { export class GuildChannel extends Channel { constructor(guild: Guild, data: object); - private memberPermissions(member: GuildMember): Permissions; - private rolePermissions(role: Role): Permissions; + private memberPermissions(member: GuildMember): Readonly; + private rolePermissions(role: Role): Readonly; public readonly calculatedPosition: number; public readonly deletable: boolean; @@ -513,7 +536,7 @@ declare module 'discord.js' { public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string; - public readonly permissions: Permissions; + public readonly permissions: Readonly; public readonly presence: Presence; public roles: GuildMemberRoleStore; public user: User; @@ -524,8 +547,7 @@ declare module 'discord.js' { public edit(data: GuildMemberEditData, reason?: string): Promise; public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean; public kick(reason?: string): Promise; - public missingPermissions(permissions: PermissionResolvable, explicit?: boolean): PermissionString[]; - public permissionsIn(channel: ChannelResolvable): Permissions; + public permissionsIn(channel: ChannelResolvable): Readonly; public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; public setNickname(nickname: string, reason?: string): Promise; @@ -710,29 +732,18 @@ declare module 'discord.js' { export class PermissionOverwrites { constructor(guildChannel: GuildChannel, data: object); - public allowed: Permissions; + public allow: Readonly; public readonly channel: GuildChannel; - public denied: Permissions; + public deny: Readonly; public id: Snowflake; public type: OverwriteType; public delete(reason?: string): Promise; public toJSON(): object; } - export class Permissions { - constructor(permissions: PermissionResolvable); - - public bitfield: number; - public add(...permissions: PermissionResolvable[]): this; - public freeze(): this; + export class Permissions extends BitField { public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; - public missing(permissions: PermissionResolvable, checkAdmin?: boolean): PermissionString[]; - public remove(...permissions: PermissionResolvable[]): this; - public serialize(checkAdmin?: boolean): PermissionObject; - public toArray(checkAdmin?: boolean): PermissionString[]; - public toJSON(): object; - public valueOf(): number; - public [Symbol.iterator](): IterableIterator; + public has(bit: BitFieldResolvable): boolean; public static ALL: number; public static DEFAULT: number; @@ -743,6 +754,7 @@ declare module 'discord.js' { export class Presence { constructor(client: Client, data: object); public activity: Activity; + public flags: Readonly; public status: 'online' | 'offline' | 'idle' | 'dnd'; public readonly user: User; public readonly member?: GuildMember; @@ -807,14 +819,14 @@ declare module 'discord.js' { public readonly members: Collection; public mentionable: boolean; public name: string; - public permissions: Permissions; + public permissions: Readonly; public readonly position: number; public rawPosition: number; public comparePositionTo(role: Role): number; public delete(reason?: string): Promise; public edit(data: RoleData, reason?: string): Promise; public equals(role: Role): boolean; - public permissionsIn(channel: ChannelResolvable): Permissions; + public permissionsIn(channel: ChannelResolvable): Readonly; public setColor(color: ColorResolvable, reason?: string): Promise; public setHoist(hoist: boolean, reason?: string): Promise; public setMentionable(mentionable: boolean, reason?: string): Promise; @@ -959,6 +971,11 @@ declare module 'discord.js' { public once(event: string, listener: Function): this; } + export class Speaking extends BitField { + public static resolve(permission: BitFieldResolvable): number; + public static FLAGS: Record; + } + export class Structures { static get(structure: K): Extendable[K]; static get(structure: string): Function; @@ -1097,7 +1114,7 @@ declare module 'discord.js' { private reconnect(token: string, endpoint: string): void; private sendVoiceStateUpdate(options: object): void; private setSessionID(sessionID: string): void; - private setSpeaking(value: boolean): void; + private setSpeaking(value: BitFieldResolvable): void; private setTokenAndEndpoint(token: string, endpoint: string): void; private updateChannel(channel: VoiceChannel): void; @@ -1106,7 +1123,7 @@ declare module 'discord.js' { public readonly dispatcher: StreamDispatcher; public player: object; public receiver: VoiceReceiver; - public speaking: boolean; + public speaking: Readonly; public status: VoiceStatus; public voiceManager: object; public disconnect(): void; @@ -1121,7 +1138,7 @@ declare module 'discord.js' { public on(event: 'newSession', listener: () => void): this; public on(event: 'ready', listener: () => void): this; public on(event: 'reconnecting', listener: () => void): this; - public on(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public on(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public on(event: 'warn', listener: (warning: string | Error) => void): this; public on(event: string, listener: Function): this; @@ -1134,7 +1151,7 @@ declare module 'discord.js' { public once(event: 'newSession', listener: () => void): this; public once(event: 'ready', listener: () => void): this; public once(event: 'reconnecting', listener: () => void): this; - public once(event: 'speaking', listener: (user: User, speaking: boolean) => void): this; + public once(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public once(event: 'warn', listener: (warning: string | Error) => void): this; public once(event: string, listener: Function): this; } @@ -1360,6 +1377,8 @@ declare module 'discord.js' { //#region Typedefs + type ActivityFlagsString = 'INSTANCE' | 'JOIN' | 'SPECTATE' | 'JOIN_REQUEST' | 'SYNC' | 'PLAY'; + type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' @@ -1444,6 +1463,8 @@ declare module 'discord.js' { type Base64String = string; + type BitFieldResolvable = RecursiveArray> | T | number | BitField; + type BufferResolvable = Buffer | string; type ChannelCreationOverwrites = { @@ -1857,8 +1878,8 @@ declare module 'discord.js' { type PermissionResolvable = RecursiveArray | Permissions | PermissionString | number; type PermissionOverwriteOptions = { - allowed: PermissionResolvable; - denied: PermissionResolvable; + allow: PermissionResolvable; + deny: PermissionResolvable; id: UserResolvable | RoleResolvable; }; @@ -1924,6 +1945,8 @@ declare module 'discord.js' { highWaterMark?: number; }; + type SpeakingString = 'SPEAKING' | 'SOUNDSHARE'; + type StreamType = 'unknown' | 'converted' | 'opus' | 'ogg/opus' | 'webm/opus'; type StringResolvable = string | string[] | any; From d91deefd79ec621c73bd1e6e02afb9d939e67886 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 21 Aug 2018 12:05:33 +0200 Subject: [PATCH 0848/1359] fix(RequestHandler): DiscordAPIError#path should be the path, not the route --- src/rest/handlers/RequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js index ce0456e1d..464c71781 100644 --- a/src/rest/handlers/RequestHandler.js +++ b/src/rest/handlers/RequestHandler.js @@ -107,7 +107,7 @@ class RequestHandler { } else { parseResponse(res).then(data => { item.reject(res.status >= 400 && res.status < 500 ? - new DiscordAPIError(item.request.route, data, item.request.method) : res); + new DiscordAPIError(item.request.path, data, item.request.method) : res); }, item.reject); finish(); } From 55c58b60e769c0b6582059b55944d5d25f47ac07 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 21 Aug 2018 16:12:01 +0200 Subject: [PATCH 0849/1359] typings: fix PermissionResolvable typedef, allow resolving of Readonly> --- typings/index.d.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 67fd0f459..3501f7e9d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -131,7 +131,7 @@ declare module 'discord.js' { public fetchInvite(invite: InviteResolvable): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhook(id: Snowflake, token?: string): Promise; - public generateInvite(permissions?: number | PermissionResolvable[]): Promise; + public generateInvite(permissions?: PermissionResolvable): Promise; public login(token?: string): Promise; public sweepMessages(lifetime?: number): number; public toJSON(): object; @@ -743,12 +743,11 @@ declare module 'discord.js' { export class Permissions extends BitField { public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; - public has(bit: BitFieldResolvable): boolean; public static ALL: number; public static DEFAULT: number; public static FLAGS: PermissionFlags; - public static resolve(permission: PermissionResolvable): number; + public static resolve(permission?: PermissionResolvable): number; } export class Presence { @@ -1463,13 +1462,13 @@ declare module 'discord.js' { type Base64String = string; - type BitFieldResolvable = RecursiveArray> | T | number | BitField; + type BitFieldResolvable = RecursiveArray>> | T | number | Readonly>; type BufferResolvable = Buffer | string; type ChannelCreationOverwrites = { - allow?: PermissionResolvable[] | number; - deny?: PermissionResolvable[] | number; + allow?: PermissionResolvable | number; + deny?: PermissionResolvable | number; id: RoleResolvable | UserResolvable; }; @@ -1875,7 +1874,7 @@ declare module 'discord.js' { interface RecursiveArray extends Array> { } - type PermissionResolvable = RecursiveArray | Permissions | PermissionString | number; + type PermissionResolvable = BitFieldResolvable; type PermissionOverwriteOptions = { allow: PermissionResolvable; From 19c298f5cc131f152befa80eb4e956e52feaa778 Mon Sep 17 00:00:00 2001 From: 1Computer1 Date: Tue, 21 Aug 2018 12:22:29 -0400 Subject: [PATCH 0850/1359] refactor: rewrite message creation (#2774) * Rework createMessage - MessageAttachment is now structurally similar to FileOptions - No longer mutates the object passed as options - Supports more permutations of arguments * Ignore complexity warning * Refactor name finding * Fix typo * Update typings * Default name to null for MessageAttachment * Make Message#reply use transformOptions * Move transformOptions * Fix Message#reply * Fix mutation * Update tests * Fix options passing * Refactor into APIMessage * Fix webhook send * Expose APIMessage * Add documentation * Add types * Fix type doc * Fix another type doc * Fix another another type doc (is this one even right!?) * Remove trailing comma * Properly clone split options * Add support for sending file as stream * Missed a doc * Resolve files only once when splitting messages * This looks nicer * Assign directly * Don't cache data and files * Missing return type * Use object spread instead Object.assign * Document constructors * Crawl is a little dot * comp pls * tests: sanitize local file path, disable no-await-in-loop --- src/index.js | 1 + src/structures/APIMessage.js | 292 ++++++++++++++++++ src/structures/Message.js | 29 +- src/structures/MessageAttachment.js | 78 ++--- src/structures/MessageEmbed.js | 10 +- src/structures/Webhook.js | 29 +- src/structures/interfaces/TextBasedChannel.js | 41 ++- src/structures/shared/CreateMessage.js | 126 -------- src/structures/shared/SendMessage.js | 23 -- src/structures/shared/index.js | 4 - src/util/DataResolver.js | 2 +- test/blobReach.png | Bin 0 -> 10423 bytes test/sendtest.js | 140 +++++++++ typings/index.d.ts | 60 +++- 14 files changed, 551 insertions(+), 284 deletions(-) create mode 100644 src/structures/APIMessage.js delete mode 100644 src/structures/shared/CreateMessage.js delete mode 100644 src/structures/shared/SendMessage.js delete mode 100644 src/structures/shared/index.js create mode 100644 test/blobReach.png create mode 100644 test/sendtest.js diff --git a/src/index.js b/src/index.js index fd2239ade..1c9f068ce 100644 --- a/src/index.js +++ b/src/index.js @@ -51,6 +51,7 @@ module.exports = { // Structures Base: require('./structures/Base'), Activity: require('./structures/Presence').Activity, + APIMessage: require('./structures/APIMessage'), CategoryChannel: require('./structures/CategoryChannel'), Channel: require('./structures/Channel'), ClientApplication: require('./structures/ClientApplication'), diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js new file mode 100644 index 000000000..77eb4234b --- /dev/null +++ b/src/structures/APIMessage.js @@ -0,0 +1,292 @@ +const DataResolver = require('../util/DataResolver'); +const MessageEmbed = require('./MessageEmbed'); +const MessageAttachment = require('./MessageAttachment'); +const { browser } = require('../util/Constants'); +const Util = require('../util/Util'); +const { RangeError } = require('../errors'); + +/** + * Represents a message to be sent to the API. + */ +class APIMessage { + /** + * @param {MessageTarget} target - The target for this message to be sent to + * @param {MessageOptions|WebhookMessageOptions} options - Options passed in from send + */ + constructor(target, options) { + /** + * The target for this message to be sent to + * @type {MessageTarget} + */ + this.target = target; + + /** + * Options passed in from send + * @type {MessageOptions|WebhookMessageOptions} + */ + this.options = options; + } + + /** + * Whether or not the target is a webhook + * @type {boolean} + * @readonly + */ + get isWebhook() { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + return this.target instanceof Webhook || this.target instanceof WebhookClient; + } + + /** + * Whether or not the target is a user + * @type {boolean} + * @readonly + */ + get isUser() { + const User = require('./User'); + const GuildMember = require('./GuildMember'); + return this.target instanceof User || this.target instanceof GuildMember; + } + + /** + * Makes the content of this message. + * @returns {string|string[]} + */ + makeContent() { // eslint-disable-line complexity + const GuildMember = require('./GuildMember'); + + // eslint-disable-next-line eqeqeq + let content = Util.resolveString(this.options.content == null ? '' : this.options.content); + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; + const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; + const splitOptions = isSplit ? { ...this.options.split } : undefined; + + let mentionPart = ''; + if (this.options.reply && !this.isUser && this.target.type !== 'dm') { + const id = this.target.client.users.resolveID(this.options.reply); + mentionPart = `<@${this.options.reply instanceof GuildMember && this.options.reply.nickname ? '!' : ''}${id}>, `; + if (isSplit) { + splitOptions.prepend = `${mentionPart}${splitOptions.prepend || ''}`; + } + } + + if (content || mentionPart) { + if (isCode) { + const codeName = typeof this.options.code === 'string' ? this.options.code : ''; + content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content, true)}\n\`\`\``; + if (isSplit) { + splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; + splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; + } + } else if (mentionPart) { + content = `${mentionPart}${content}`; + } + + const disableEveryone = typeof this.options.disableEveryone === 'undefined' ? + this.target.client.options.disableEveryone : + this.options.disableEveryone; + if (disableEveryone) { + content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + } + + if (isSplit) { + content = Util.splitMessage(content, splitOptions); + } + } + + return content; + } + + /** + * Resolves data. + * @returns {Object} + */ + resolveData() { + const content = this.makeContent(); + const tts = Boolean(this.options.tts); + let nonce; + if (typeof this.options.nonce !== 'undefined') { + nonce = parseInt(this.options.nonce); + if (isNaN(nonce) || nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); + } + + const embedLikes = []; + if (this.isWebhook) { + if (this.options.embeds) { + embedLikes.push(...this.options.embeds); + } + } else if (this.options.embed) { + embedLikes.push(this.options.embed); + } + const embeds = embedLikes.map(e => new MessageEmbed(e)._apiTransform()); + + let username; + let avatarURL; + if (this.isWebhook) { + username = this.options.username || this.target.name; + if (this.options.avatarURL) avatarURL = this.options.avatarURL; + } + + return { + content, + tts, + nonce, + embed: this.options.embed === null ? null : embeds[0], + embeds, + username, + avatar_url: avatarURL, + }; + } + + /** + * Resolves files. + * @returns {Promise} + */ + resolveFiles() { + const embedLikes = []; + if (this.isWebhook) { + if (this.options.embeds) { + embedLikes.push(...this.options.embeds); + } + } else if (this.options.embed) { + embedLikes.push(this.options.embed); + } + + const fileLikes = []; + if (this.options.files) { + fileLikes.push(...this.options.files); + } + for (const embed of embedLikes) { + if (embed.files) { + fileLikes.push(...embed.files); + } + } + + return Promise.all(fileLikes.map(f => this.constructor.resolveFile(f))); + } + + /** + * Resolves a single file into an object sendable to the API. + * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file + * @returns {Object} + */ + static async resolveFile(fileLike) { + let attachment; + let name; + + const findName = thing => { + if (typeof thing === 'string') { + return Util.basename(thing); + } + + if (thing.path) { + return Util.basename(thing.path); + } + + return 'file.jpg'; + }; + + const ownAttachment = typeof fileLike === 'string' || + fileLike instanceof (browser ? ArrayBuffer : Buffer) || + typeof fileLike.pipe === 'function'; + if (ownAttachment) { + attachment = fileLike; + name = findName(attachment); + } else { + attachment = fileLike.attachment; + name = fileLike.name || findName(attachment); + } + + const resource = await DataResolver.resolveFile(attachment); + return { attachment, name, file: resource }; + } + + /** + * Partitions embeds and attachments. + * @param {Array} items Items to partition + * @returns {Array} + */ + static partitionMessageAdditions(items) { + const embeds = []; + const files = []; + for (const item of items) { + if (item instanceof MessageEmbed) { + embeds.push(item); + } else if (item instanceof MessageAttachment) { + files.push(item); + } + } + + return [embeds, files]; + } + + /** + * Transforms the user-level arguments into a final options object. Passing a transformed options object alone into + * this method will keep it the same, allowing for the reuse of the final options object. + * @param {StringResolvable} [content=''] Content to send + * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto transformed options + * @param {boolean} [isWebhook=false] Whether or not to use WebhookMessageOptions as the result + * @returns {MessageOptions|WebhookMessageOptions} + */ + static transformOptions(content, options, extra = {}, isWebhook = false) { + if (!options && typeof content === 'object' && !(content instanceof Array)) { + options = content; + content = ''; + } + + if (!options) { + options = {}; + } + + if (options instanceof MessageEmbed) { + return isWebhook ? { content, embeds: [options], ...extra } : { content, embed: options, ...extra }; + } + + if (options instanceof MessageAttachment) { + return { content, files: [options], ...extra }; + } + + if (options instanceof Array) { + const [embeds, files] = this.partitionMessageAdditions(options); + return isWebhook ? { content, embeds, files, ...extra } : { content, embed: embeds[0], files, ...extra }; + } else if (content instanceof Array) { + const [embeds, files] = this.partitionMessageAdditions(content); + if (embeds.length || files.length) { + return isWebhook ? { embeds, files, ...extra } : { embed: embeds[0], files, ...extra }; + } + } + + return { content, ...options, ...extra }; + } + + /** + * Creates an `APIMessage` from user-level arguments. + * @param {MessageTarget} target Target to send to + * @param {StringResolvable} [content=''] Content to send + * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use + * @param {MessageOptions|WebhookMessageOptions} [extra={}] - Extra options to add onto transformed options + * @returns {MessageOptions|WebhookMessageOptions} + */ + static create(target, content, options, extra = {}) { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + + const isWebhook = target instanceof Webhook || target instanceof WebhookClient; + const transformed = this.transformOptions(content, options, extra, isWebhook); + return new this(target, transformed); + } +} + +module.exports = APIMessage; + +/** + * A target for a message. + * @typedef {TextChannel|DMChannel|GroupDMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget + */ + +/** + * Additional items that can be sent with a message. + * @typedef {MessageEmbed|MessageAttachment|Array} MessageAdditions + */ diff --git a/src/structures/Message.js b/src/structures/Message.js index 96ffacbb7..e229e20cf 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -10,7 +10,7 @@ const { MessageTypes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); -const { createMessage } = require('./shared'); +const APIMessage = require('./APIMessage'); /** * Represents a message on Discord. @@ -359,7 +359,7 @@ class Message extends Base { /** * Edits the content of the message. - * @param {StringResolvable} [content] The new content for the message + * @param {StringResolvable} [content=''] The new content for the message * @param {MessageEditOptions|MessageEmbed} [options] The options to provide * @returns {Promise} * @example @@ -368,17 +368,8 @@ class Message extends Base { * .then(msg => console.log(`Updated the content of a message to ${msg.content}`)) * .catch(console.error); */ - async edit(content, options) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = null; - } else if (!options) { - options = {}; - } - if (!options.content) options.content = content; - - const { data } = await createMessage(this, options); - + edit(content, options) { + const data = APIMessage.create(this, content, options).resolveData(); return this.client.api.channels[this.channel.id].messages[this.id] .patch({ data }) .then(d => { @@ -467,8 +458,8 @@ class Message extends Base { /** * Replies 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|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example * // Reply to a message @@ -477,13 +468,7 @@ class Message extends Base { * .catch(console.error); */ 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 })); + return this.channel.send(APIMessage.transformOptions(content, options, { reply: this.member || this.author })); } /** diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 29f730764..a8d74ee67 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -2,77 +2,41 @@ const Util = require('../util/Util'); /** * Represents an attachment in a message. - * @param {BufferResolvable|Stream} file The file - * @param {string} [name] The name of the file, if any */ class MessageAttachment { - constructor(file, name, data) { - this.file = null; + /** + * @param {BufferResolvable|Stream} attachment The file + * @param {string} [name=null] The name of the file, if any + * @param {Object} [data] Extra data + */ + constructor(attachment, name = null, data) { + this.attachment = attachment; + this.name = name; if (data) this._patch(data); - if (name) this.setAttachment(file, name); - else this._attach(file); } /** - * The name of the file - * @type {?string} - * @readonly - */ - get name() { - return this.file.name; - } - - /** - * The file - * @type {?BufferResolvable|Stream} - * @readonly - */ - get attachment() { - return this.file.attachment; - } - - /** - * Sets the file of this attachment. - * @param {BufferResolvable|Stream} file The file - * @param {string} name The name of the file - * @returns {MessageAttachment} This attachment - */ - setAttachment(file, name) { - this.file = { attachment: file, name }; + * Sets the file of this attachment. + * @param {BufferResolvable|Stream} attachment The file + * @param {string} [name=null] The name of the file, if any + * @returns {MessageAttachment} This attachment + */ + setFile(attachment, name = null) { + this.attachment = attachment; + this.name = name; return this; } /** - * Sets the file of this attachment. - * @param {BufferResolvable|Stream} attachment The file - * @returns {MessageAttachment} This attachment - */ - setFile(attachment) { - this.file = { attachment }; - return this; - } - - /** - * Sets the name of this attachment. - * @param {string} name The name of the image - * @returns {MessageAttachment} This attachment - */ + * Sets the name of this attachment. + * @param {string} name The name of the file + * @returns {MessageAttachment} This attachment + */ setName(name) { - this.file.name = name; + this.name = name; return this; } - /** - * Sets the file of this attachment. - * @param {BufferResolvable|Stream} file The file - * @param {string} name The name of the file - * @private - */ - _attach(file, name) { - if (typeof file === 'string') this.file = file; - else this.setAttachment(file, name); - } - _patch(data) { /** * The ID of this attachment diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 72e22464e..2d0208b24 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -1,4 +1,3 @@ -const MessageAttachment = require('./MessageAttachment'); const Util = require('../util/Util'); const { RangeError } = require('../errors'); @@ -141,14 +140,8 @@ class MessageEmbed { * @type {Array} */ this.files = []; - if (data.files) { - this.files = data.files.map(file => { - if (file instanceof MessageAttachment) { - return typeof file.file === 'string' ? file.file : Util.cloneObject(file.file); - } - return file; - }); + this.files = data.files; } } @@ -203,7 +196,6 @@ class MessageEmbed { * @returns {MessageEmbed} */ attachFiles(files) { - files = files.map(file => file instanceof MessageAttachment ? file.file : file); this.files = this.files.concat(files); return this; } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 088806ca2..711b3db79 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,6 @@ const DataResolver = require('../util/DataResolver'); const Channel = require('./Channel'); -const { createMessage } = require('./shared'); +const APIMessage = require('./APIMessage'); /** * Represents a webhook. @@ -82,11 +82,10 @@ class Webhook { * it exceeds the character limit. If an object is provided, these are the options for splitting the message. */ - /* eslint-disable max-len */ /** * Sends a message with this webhook. - * @param {StringResolvable} [content] The content to send - * @param {WebhookMessageOptions|MessageEmbed|MessageAttachment|MessageAttachment[]} [options={}] The options to provide + * @param {StringResolvable} [content=''] The content to send + * @param {WebhookMessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example * // Send a basic message @@ -127,20 +126,18 @@ class Webhook { * .catch(console.error); */ async send(content, options) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = null; - } else if (!options) { - options = {}; - } - if (!options.content) options.content = content; - - const { data, files } = await createMessage(this, options); - + const apiMessage = APIMessage.create(this, content, options); + const data = apiMessage.resolveData(); 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 } : {}; + let opt; + if (i === data.content.length - 1) { + opt = { embeds: data.embeds, files: apiMessage.options.files }; + } else { + opt = {}; + } + 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); @@ -149,7 +146,7 @@ class Webhook { return messages; } - + const files = await apiMessage.resolveFiles(); return this.client.api.webhooks(this.id, this.token).post({ data, files, query: { wait: true }, diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index b7b487dfa..207f74248 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,8 +1,8 @@ const MessageCollector = require('../MessageCollector'); -const Shared = require('../shared'); const Snowflake = require('../../util/Snowflake'); const Collection = require('../../util/Collection'); const { RangeError, TypeError } = require('../../errors'); +const APIMessage = require('../APIMessage'); /** * Interface for classes that have text-channel-like features. @@ -66,8 +66,8 @@ class TextBasedChannel { /** * Sends a message to this channel. - * @param {StringResolvable} [content] Text for the message - * @param {MessageOptions|MessageEmbed|MessageAttachment|MessageAttachment[]} [options={}] Options for the message + * @param {StringResolvable} [content=''] The content to send + * @param {MessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example * // Send a basic message @@ -107,16 +107,35 @@ class TextBasedChannel { * .then(console.log) * .catch(console.error); */ - send(content, options) { // eslint-disable-line complexity - if (!options && typeof content === 'object' && !(content instanceof Array)) { - options = content; - content = null; - } else if (!options) { - options = {}; + async send(content, options) { + const User = require('../User'); + const GuildMember = require('../GuildMember'); + if (this instanceof User || this instanceof GuildMember) { + return this.createDM().then(dm => dm.send(content, options)); } - if (!options.content) options.content = content; - return Shared.sendMessage(this, options); + const apiMessage = APIMessage.create(this, content, options); + const data = apiMessage.resolveData(); + if (data.content instanceof Array) { + const messages = []; + for (let i = 0; i < data.content.length; i++) { + let opt; + if (i === data.content.length - 1) { + opt = { tts: data.tts, embed: data.embed, files: apiMessage.options.files }; + } else { + opt = { tts: data.tts }; + } + + // eslint-disable-next-line no-await-in-loop + const message = await this.send(data.content[i], opt); + messages.push(message); + } + return messages; + } + + const files = await apiMessage.resolveFiles(); + return this.client.api.channels[this.id].messages.post({ data, files }) + .then(d => this.client.actions.MessageCreate.handle(d).message); } /** diff --git a/src/structures/shared/CreateMessage.js b/src/structures/shared/CreateMessage.js deleted file mode 100644 index 8ba66c342..000000000 --- a/src/structures/shared/CreateMessage.js +++ /dev/null @@ -1,126 +0,0 @@ -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'); -const { RangeError } = require('../../errors'); - -// 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 WebhookClient = require('../../client/WebhookClient'); - - const webhook = channel instanceof Webhook || channel instanceof WebhookClient; - - if (typeof options.nonce !== 'undefined') { - options.nonce = parseInt(options.nonce); - if (isNaN(options.nonce) || options.nonce < 0) throw new RangeError('MESSAGE_NONCE_TYPE'); - } - - let { content, reply } = options; - if (options instanceof MessageEmbed) options = webhook ? { embeds: [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); - 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 = null; - options.content = ''; - } - } - - if (options.split && typeof options.split !== 'object') options.split = {}; - let mentionPart = ''; - if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { - const id = channel.client.users.resolveID(reply); - mentionPart = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>, `; - if (options.split) options.split.prepend = `${mentionPart}${options.split.prepend || ''}`; - } - - if (content || mentionPart) { - options.content = Util.resolveString(content || ''); - // 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 = `${mentionPart}\`\`\`${typeof options.code !== 'boolean' ? - options.code || '' : ''}\n${options.content}\n\`\`\``; - if (options.split) { - options.split.prepend = - `${options.split.prepend || ''}\`\`\`${typeof options.code !== 'boolean' ? options.code || '' : ''}\n`; - - options.split.append = `\n\`\`\`${options.split.append || ''}`; - } - } else if (mentionPart) { - options.content = mentionPart + (options.content || ''); - } - - // 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; - }) - )); - } - - if (webhook) { - if (!options.username) options.username = this.name; - if (options.avatarURL) options.avatar_url = options.avatarURL; - } - - return { data: { - content: options.content, - tts: options.tts, - nonce: options.nonce, - embed: options.embed, - embeds: options.embeds, - username: options.username, - avatar_url: options.avatar_url, - }, files }; -}; diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js deleted file mode 100644 index 95ea49ca3..000000000 --- a/src/structures/shared/SendMessage.js +++ /dev/null @@ -1,23 +0,0 @@ -const createMessage = require('./CreateMessage'); - -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)); - - 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); -}; diff --git a/src/structures/shared/index.js b/src/structures/shared/index.js deleted file mode 100644 index 5e81ec3c0..000000000 --- a/src/structures/shared/index.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - sendMessage: require('./SendMessage'), - createMessage: require('./CreateMessage'), -}; diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 95e0aa5ad..5fcfd0d43 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -103,7 +103,7 @@ class DataResolver { }); }); } - } else if (resource.pipe && typeof resource.pipe === 'function') { + } else if (typeof resource.pipe === 'function') { return new Promise((resolve, reject) => { const buffers = []; resource.once('error', reject); diff --git a/test/blobReach.png b/test/blobReach.png new file mode 100644 index 0000000000000000000000000000000000000000..358bf47397bd25facfbe2c3f7f03071159f672a5 GIT binary patch literal 10423 zcmZ`^lh`I4;c zeI;3mR#lckLBdCZfPg@ela*BeC!POy5#avu(Y;57e*(@-K}Hhd>wl-Hw;~k+f>uUO zQcTlp<0{`f&rmDQcs^iOy!f;%JBom- z#;k$;s}AjR8{5x|*A?U;lax^83mvMnp$>Ui+sMIm$A-t7)5c}>Y$KX z@TulDxcBtB_vSOd^*QHx<7YKt$(h2*G4TK!KR-X*9%qF)gtFWJqmX{kq;A@=+!hW< z%{AlwtYdX!Q-w!{$_)Ypg%8AwLVA&y0CxqTv(yMyn2TV&ki7gi-kmk8HyIBR{Why6r(Mcr!;X1@*J=$M5J_-+98QX7^! zHTQ$Kp=)J!Yq;=ShXj6d=Sv)JkQ4zf5slV%?m~f4F>ICqvqqUA0Dk&c7?C&c#{(b;2Z5orICZ&3a8y~iFanF zB`tBDqq-x0B{XdE^<)e7!!iVgJ0gaCi{7m} zmNj9}5)I?rk$LOcmfbsG^6%Z zR?Ws7ZZ)w7Vsg`2@zc&O-z49*!>H9Jky1oE;jBFDPR;&6blh}id;_;_?4)S)S|V3D z1si-P>DIe6t)F@0fOTsd$s1pL%_HUXZnawqMJp9qV<>ehBA&zl%RfNRJ@?ovIsg;2 zCnc6Phj&9^%su)T&2=GlDjj2Sx33@vO}TOPzy0Uv}n zV5X_IJzqrfWg_&*ljP_6_wK|n&s_NyXPq9$rgn-`&5u-;Gz#DcoA**g`jc4HpZ@Yl z6iUU;>FRH2)K5SmE}QPXnFX=-srSupmWQacyyPc3y3Rn zTr5G>307rz2&UW%q4l}$iria*_;~%psosRXMWbC;Vu5Wao&z!py48^wP$hY*rJ7w% z&Qcnl=w(11i6~?;bPvl}OU+9RzdG)M?+B8~_?s>ZHjWV^N~cJ3`fCM#;#XGwJkOfi z+D`>`ecSkn98(3Mdbks{MFvz+s(hEhg(&uoBi&sj4!?H1aRbYZu^kB`FzX?HX8}y! z0YDQ2HZ%&71Ix!0g)#fJnZz5GzM9qqok&IXi2Oegj_I}&#W9Ip;D?TaTZnbj;jGJI zmL*()+}@}Tb`VsUs7ORx$YHw~uKb!E-Gc|A z^3A4!_fsm0RZL|HLpSpxE*4R2rSMe6l zWpiIjTmr$&PeL-DzW0b8i0^ku-E9`VPWC8DCcD(hy>c1w#!J%8YgaCNYRSC^7g%uH zVu86PXh0Z}k~`vxK1PKhxDUIzC7)0w%KHiX+MIps#)!W&{z6~CZ zx=wv5K-S%df`<>sn~%x|Gi35bm=@?Hs8Q0N|L(Xou9?EpW|hXzB)CxpsG>Ur z;W@ibl!=K=8}_eKseC!LUR^fdu~Sl7uS$*UK-#JPZG97^#4=4euKN?8J-YB(HO7)jZ?OsovE2+BCO^qxdV5a3>kKa}BECGCnhxFJSo(c@KtETA2JgM9{ zef!>f`LBCu;5~@5&t+7_4Eg&S)&XsNL)COp7+y+%F+ zR@x+$8f}EQHC3>4=3QoB;$kej+Uz zO*2_SO>^AD@C0d8gx$Y-8TXaHEZir}_Nw=UV4jEt4(~H83VB1wyd=VDE9Gzn zzWEXt;TCDe8%zLZeE6)1ac`mSAWJQuR*c}qLHlKc7Xo!NlQz8_ATu18^@{@XLbe0P zCW{=RH@rlg%&xfg%2Tg}iYNZ<9 z2u(HhGse=U$V&0$#|VXx{n$N1LV|lkPhV#zdi3oSM?sx68 zMGvt8RerZ-*k`}o?1h6PH8pk8UUQq3riR)wn5hRaD5>=`i{=Gcc+p=#B8|Mn006emYF~H?) z-BPi%WAZ3|0_jzkYdm*-L)G8-%A$;~Lk^A-{Uw-(4WPH0b zb8jx(i09^fYj-D9N88MAcH&M|do_W^MURE5{Bm4jG5k2O+rqJz^G)91hbbO?h+8TU zJ_a(b$;yAc+(^OPP>JAJr(10x#!Pa3fk2gzV`+KhcPTB$nG=p?4+D95+3U`>z|RY} zn;A#jK3m0CI(~2D-SA6;MX@_`B9bya(5?Bo!)Q-Y3MEvZeI@X==VJ&%t?2PP;yNuX z6F{6gNS;x!nQ!m$(ERgUg5oHb&~b#Y?NKoA@6sx=8|sCpehi!B3dwypIbCL)R*|Na z)AOQ*ni5*HCvmdLk3&I!&*x@DOjTiCQ@F1qT3>HmvZ}@P2YMQdm_KYf+LDWCmt<0> zVOg3cTelex47HmEN3pwAB^9fxnMz!Ju3D9v`|kjT_p8VVy@pcxrm6C(kjOWLKHLar zBV49s#b|xu)4@Ja=~?O9cq(FrXo);L3-zF_oI^l%X9~}Rh#OoI#qO(om7DbNDWK&qkmUZ$UI)FnS5mt?%Dch>}lKR3-SQ^#xUB9@ScVeqO6 zZOUyCnkE)APLx?TdxByu)FoXJ_pi8TcSXGzSvKW=x85H`|hr=Jssx|{V}784z^H4aGeD8H;xm4X`4MrAyD&`5@y?$Ogux+ zd^I2oWM>}COnyXCH0)uJRlk|NjQwJ;fW~D&%pP#tIKWMXeO82or>`7Ls$4|RoG?fW z{CMi;_8R-|^TP(^4Q4-A`$HmI@fP;2n%I`o5SfxnOkUhAe_;!=qC^G8SRM-- zTH_QVPqr04gC*+B=H7k-OVt?SRNcAkq#GrU9Ig#TmswZ;cu1oLEmR>3gU$~5Rq}4s zWcaA3zFrYnUd8Y1uCYH*h)G^(;{&>KJGFCt{MM1hIml1OzrSoXF=gAf~C(oD*8w8Hr9 zt_`r6l7aaBU9d(6bW)z{xZ!(40#OGKq($|EnUB}zqT^g2`3U9-q3+_T4rod3`1;z| z(?C&sahXV)PAuops^U*#9M&8tjapUJFu$;g)Iq+|5qs@1e;%+rL%uqGha^R~kvK+#ZoU8|)AG&Z zq|+>2aQR(R;~Br5`5!v?KrFNePVs9k8Zqqw#rn=$o~g?tJ*cCp}daf)PGxS)u(( zvKr?$L1l@{-EqGZ%p*1$l-NsS9*@ZTtAHK%(h!;e->1;c?OB!S7^dHqJN?-XNF4)u zAodI4v9i;qAsBoP81@XvDIq)c2A5ejc_)g5jSa6zAH1XEE^}QiTwx>46nwqytJQav z4grK98$+*ufbnAKIorH(St(f{z5b3;hv-OJ@S)Dy@l|3xVDI5L7m>2Q`v(=Y!hdN# zP+@d(_<3Upq4x7q@8tY>syx60 zemqVXJ@$p;~LBOGJ zJ(+e~&Ya;qFX)CIXY_?N=cpidO~2`&IyoCXPp(d zfXp|K=uq&4A7=RbULF|3Yf~t1Kie}|5nn@VbM-g%F^;gBkiOf`UmiQM-4cRQvfUHc z_rcL(JsK$NWx8j#Sf_D{{*G&x31Q{@GA8K- zJ4wqIOBUDK3s=7(I9OL{zeEEgn5 z5%J}Ur~v|T`o^6xYadalR+ z{#6hlx%gV*pxI`R^!CBRbDs?2yuSl+-QF(ZDM}l2EBg-ep|jHGN5VKevzk1dB)UvF zkQ@M3#mrG7(D(92zi|SUJ*cECCLe}w8&z%G^Lk6=P10CZiGBEgS~rLDOWjY_y&SzR z%)m+(b9JwjE!I;RhrBw2txfZ1nhQ-EcXK1LF9uB?CHkn zYR=04JwtgFEli0OxqCR6fBa$E^5*b+W{5o;^F1%B^XWU9fLnQEP2I53C=0*Nmqce4 zt`gUA3*nkv37E=f;4sAzJGQ_7&F70sC(SH} z!iQGBeY8h@9a%lsTL#Q&-nL*9lJvUfxv5kh)M)Hw6^!t)iEkT0gpw9jRYlLvD)S2n z*aCs6ya{p`Wz9mQ69C5yj*8039I&5K_VJ@?+Lin2%OluVZ4Y! zxNyyB(_?%#sw&hNGqbt6<4tY*I0f(nFQ4J>r*s4hE<}aCvn6X~fR$da6ciY<3r)~B`uyb4O7ma_w-Hz6+`{+ zv@%~=`-K6jV{)+o{gYpUpRyVKuU0~df&kSjanI&ca3C-Z$jlrHNTF+Spg|FF8VNYz zD1M}gW6?iYaaf!4`VtcPG?4H={BN{Iz1@Fcu77Mn z#0(dgt9BQ?!N1*dVmfT)N8yU~{Dj9OP(LgLkAu*^8|pfcJd|tFMsAJ6iZ@U%z5nkV z?KkX^4pT^$wI25N;l_irs2#DXYqnUQVt$*~nlJhWSAWq?`f0l-zx@x2_YDGhRb`?x z(9*UFSBVl8=HTt45rpUE8r`vLv;lMbrGoLF-N2{qo1NEU*D}|$wSwez1`@yD?}Pir zFH!NXGq9jj=IY64HvsmmEE+o881jFAQ6x$-qznr(s8I{#W>C-}m0&bZvv;q(UZayL z+QTtjCk4PPcbTgRuJ1j#Joc(e`9%jsRl|!mdj-pQwZSL?P>#No!_SZko%H|S^6OvJ z1n2p1toMdrT1NWd$$1FcbDh}gCZ8wsTK(1#hWd_>iUR$YV6gx7BG2OsYSK&u@9C{D z-6m;no#sAtW}@5G<|ralC62am!G0{yskcuq6#)`Rq-rP<#MtC)6&DL6j-4=*QWBz| z2G#m79-P28=kfBK5P=u~%XD%~dCej(YqbaH!XL|UeLgxFC;Vk8qA3X0%(b`HH_Wbo zY_Z-e_YQTKPp?pmbn+%*spbiwg8KT1#B_9e3)xCI-2TmS^Ll`*%suoa<`HWW+|w2^ zQ$q{uh)zP?;{GM#u&lRT98(q?1^xYe;1UAyYk$Te-D3$I+HY3;2JF#!x(dHme>(q> z!%`M$MABa5s%cX=d;91}F(D8l@m<9w_<__8nr5%reyHcc^!`eh>ayZ}j~b*|G+(S# z&sBj{yTZ~sF7UlJx<5(?+Y58A zUzF^wTS?Us_L9daKF=b(13Jkl5z=__@gD};U`gU_`3rQlQq>)>^m#Qr%N7b3axL zD*7Kzellifl6&-dRP4VmnmH-`RosEF~dH|BVb|0R{lieXC(_^rok` zac*WwnxODDJcA~d&U1~5U~?x1#FqPF{I86YlVC+^6BEr=C)%Rsk{o=n%v_biK(*=f zbQo~gptNz%`=~0(m*m@-<5OPW?f7OunFekOwei(glFves7diYA+4op=fIJl|HfqLE z^!4Grt^WQnDri9V*23Iha@UzW(Q%w=>hxDIo6O1T7J(cH$*qg(7)v=z`j}e+Xn62fXEn8$1RFo8|}Jn_MAP^H*W~$ zdg}Q^G7`-c7--h~5I~WGiK}zTQK|4UN! z7vMeC;3wymxKxmI^Ygt5io=(P;d0u%jI{Lc>uV#o_jlN@3@{Rq2mWM-tMg}KqS0B_ z-1~XcB;5nXtg(Js_b=0gj*+t|6@X>tn8MIp0cUY$t@TqxlJBa;?@bEi1SE;Wp#rn> z+ubdv&f{Xn)!hQ?Rma$gvseyO^}^CDT=#J&!L!=ZrG~OL9`f87^*U`A<*(L~y5sw+ z-cX#aF7oD!5J1S~!6C*l<t&eR4att%K+5f@zLcj9H^ym|f!L z{nJgkF%m=wY0{=+x6*38|IeF^uV>#cpvB`pVAWx4tDC>uLG9(Y%jje&?j8V|>r$ZF zls_4gQZp(Ie&yu#PvQfmku$S6%zd)Da+qZ^Vc_SrixpG7xLT3j>nAkZm#c5VlK^y| zTj2N8aFHInyz&hEsK|3(6tPNNN%qPKIR8)L*v6`=vA5Gg5RRdHvb!gEZ-SB<#hODKloXZ_-tY@gLyXUcvR6WOw^P2CIUNjhzi9aMPSX`QuB7T558 zBr+xnZO>H?QsC_~xq>x~CPv5cKCwqm zJ%_jH!cn z`z)7RwK-nGYZwnJ1j4aHXzWl3vb}CPX@<5xz%+Z-D$x@@NfmJAB~^nFo?bNgo)H|e zfzSJoTxy`$g^euS-5)0H)28e%rZF7Et@a$M5ndOGXa2OM#hg|-72tG%F+zsSapvK(sFijR0L$#nm zQqIR|TtH`HYxgexK(myVG%;z>#AmT?T#1ko78 z4`eHtv#TYK4@rO}-*1zV-8P7<3(+bF_&);7(b&|*xs^SW}XStplcBGM1XV^ z+~A^}>;R;RhCX?sITlHE1{}+{g_R_2jV)mdJi6d8nVtHFAb>Kz@O~FVjDf#nw5wK! zhg61=0Q8YJq%~VqV*X1p+2RqHo!}7q2KahU!sGHskMG3d8j>Uhj3ZNd0S zF(GDSy017!aS>M01|!%>caWFLfXnAhSC2zZ20ITGdpgp>`EQVF;pL|>Q0)PU!l&Zq ze>EqTk5&?bD@H^U9k&7xLD0YWloxS2gHkj&;3cwGj31|p7^!Z4#6>LvJo3}eKIpMz zdjTn}^=5AtW^bainL1kdGxaf(wN}>Z*%=9T#Aw*)`$N+)rN4g9pZdPO zYs>-fF%g~bHc~F#Tf%R9bQ|M|F@*)p4HKoQr!)qx^3yywt%8phBfn_0_->AnRtWQ* zR=v?s!*dn`D-p8wgNu#T2({3;)ROA3N)#eeCAjOWc`ZlaK*@elp{fT9u3()!wUf*X z=zvb)nNXqQSNb>p@bh`0ZFwXMG@lIu)M6(DTm}gu^ZG3QW{#3bM?K7^Y%WRIN-wpW z#bJIUpk|Fpzm4{^)@k}$%hdf83Z#k?BCo@lTkB}axisoiHoehY)bjmn`Mzc}pXucs z6ViB*Ut#i|N~^jp&sqJ}Dq#p98;(0{O1Y+~5j35sE&UHfoBWK#ahK266t#AhwZv<^ z#AL5e>~c$|%oo2I#Ip*QgEP2CcfX&b{Hd0Y@VYw!6xZ){{^?=2W|Cj)nQAKQ|A9QJ z>FEB%3&2ZO_I-i>vN@_BJN+}voI;jukW5SW8IRk&f(dc+0aS|-R~n%j+F~9PV5VJM zD0+N^)uh+(q^}+IO63V&$|m!-*=xS(a_m|1kBq6Lk0X(8r$Q#dyg6;_)7we?VlnoZ z(bsa4oCc<2)rHF=_ZyI~sfVPVQnm2>k@Ngfsi8tS$_+D@>8FeR6d3NI)d3tze=Zxg zv4lAW*iAom-z-^g3fp1h)W1+Z^_^=aRn??EBT=6&OomULYOOa-sn;z#=Z-w2VWOrC z%Jwc>gsEgV3K=&NX}#iw6jrYJ@q`;|tlriW&M8{9HmznZn-A@>7@g{Tj3y9W-V2vLOG^{C)@(r7DlG zD^cq3f{KUVoyXd-i;0{frFplrYIoQZ5u}OQIi3n$;+`jj^*LYUOH&Cq4&YIh+*#M7t|al`HRAYa%Msx|^ZHNcuXV6*M) zj13epSp2y0_{gWo-9|)E!~gCVARwb-Nf&9&5aKb}vyCcu_G;Ya5||x!*X@w+UGiF_ zWu6GCtd5B>k+l0<(kVIj!R@coE`>NR zX2s;K3_Se}!6}MoS^Hx^o|_In;03isx0j*H-N>f-9%vTY4zI*7QG@>)Gh7q2djU#X zjoK(_P2JE`@`2G!LZ|Qu{_}Gei^jDbq#3y9d3-(lof>+hlR$YiN#<0pNn2IL*^7=b z^}kAGHK;UiQCOfW6_-#M*Wo5RkhkpfPo3`2CDc_7q+?#Y|YMNVU4}h?|zsh|3 zx$2Npv~3IF2&=m?hpDSSVql{5(KTQv@F<=BwiXsxc;1Wd$v7@+H_I0i;UgMn&TXN* zLrwL?+eCgmDl3%@vd_hoAevINIr{#3D3=fK6bzXWBB#jiKiYTh%v*Wow^f`?|H+62 zqWNu;q9w{pA?GTDv4Vg%gSzO-&((JcBkE z+DfYOP;Nc1_9^s^rGWAKm*0-!gU#=1Ebe%~3M5Dd;Ux!F#vSu)34*qDUlSs#L1SgWx&NZ&iz9}y-ps;(F;R8J{%X0=O1twba5MiRFa)3PKSCnSuaXaawG zA6F^NwWZ~4_UQC?b=H2t`jsRgRxg>x&o%6~2S3~XkYm^{uDt}aIxo4|u9e!Y)juVuO)99Hlq3tvm~~lH zPy{8^uPlQf*$RU%D~M`ik^^D*gI0er>az?*(W8{2w&mwheU`yElUUB9fHhsPvIHVq zOdc2)DA>mUw5;L@=!?BTqW@+-;(TnV#@O!{HnU@S+_|$)t~l)HXH^v{$giYZ_1NNf zI($I-tOa@0+`xE5%0m=9s12w;OUYcUa7Z(jt7SteO*Um>v zb}~^6Qfe%X5VR86CZ`)$SvjP%%4V z48(oPt@}@m4AV9hnpO{iACPMVriblC?^L=gnQfLb!DXZhrt8fEhXZ*nLu9~FQ*Cmy z_Yp)4aY(LRDR21OB_m>Gkr{TE*%1ZDH^s3K5_z~^8X7x59XALOLbPjKc1Cep(++?H zq8PeA{6QpeG+N9V0hPkOCVHg~cLOcM^!3ysLJWRz=UFI3U5riRzoU;|DZhbi%YU>a z7n7kA=L6i42W*wcAo5QBALWb04v@bfqLEMiZ^;nPT}szo!^z6s)6~@xLd?R+%#u{j d!PMGP-O|*;+hxL1@ShQcoRqR;y|_up{{j5-U}69O literal 0 HcmV?d00001 diff --git a/test/sendtest.js b/test/sendtest.js new file mode 100644 index 000000000..14d48260a --- /dev/null +++ b/test/sendtest.js @@ -0,0 +1,140 @@ +const Discord = require('../src'); +const { owner, token } = require('./auth.js'); + +const fetch = require('node-fetch'); +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const client = new Discord.Client(); + +const fill = c => Array(4).fill(c.repeat(1000)); +const buffer = l => fetch(l).then(res => res.buffer()); +const read = util.promisify(fs.readFile); +const readStream = fs.createReadStream; +const wait = util.promisify(setTimeout); + +const linkA = 'https://lolisafe.moe/iiDMtAXA.png'; +const linkB = 'https://lolisafe.moe/9hSpedPh.png'; +const fileA = path.join(__dirname, 'blobReach.png'); + +const embed = () => new Discord.MessageEmbed(); +const attach = (attachment, name) => new Discord.MessageAttachment(attachment, name); + +const tests = [ + m => m.channel.send('x'), + m => m.channel.send(['x', 'y']), + + m => m.channel.send('x', { code: true }), + m => m.channel.send('1', { code: 'js' }), + m => m.channel.send('x', { code: '' }), + + m => m.channel.send(fill('x'), { split: true }), + m => m.channel.send(fill('1'), { code: 'js', split: true }), + m => m.channel.send(fill('x'), { reply: m.author, code: 'js', split: true }), + m => m.channel.send(fill('xyz '), { split: { char: ' ' } }), + + m => m.channel.send('x', { embed: { description: 'a' } }), + m => m.channel.send({ embed: { description: 'a' } }), + m => m.channel.send({ files: [{ attachment: linkA }] }), + m => m.channel.send({ + embed: { description: 'a' }, + files: [{ attachment: linkA, name: 'xyz.png' }], + }), + + m => m.channel.send('x', embed().setDescription('a')), + m => m.channel.send(embed().setDescription('a')), + m => m.channel.send({ embed: embed().setDescription('a') }), + m => m.channel.send([embed().setDescription('a'), embed().setDescription('b')]), + + m => m.channel.send('x', attach(linkA)), + m => m.channel.send(attach(linkA)), + m => m.channel.send({ files: [linkA] }), + m => m.channel.send({ files: [attach(linkA)] }), + async m => m.channel.send(attach(await buffer(linkA))), + async m => m.channel.send({ files: [await buffer(linkA)] }), + async m => m.channel.send({ files: [{ attachment: await buffer(linkA) }] }), + m => m.channel.send([attach(linkA), attach(linkB)]), + + m => m.channel.send({ embed: { description: 'a' } }).then(m2 => m2.edit('x')), + m => m.channel.send(embed().setDescription('a')).then(m2 => m2.edit('x')), + m => m.channel.send({ embed: embed().setDescription('a') }).then(m2 => m2.edit('x')), + + m => m.channel.send('x').then(m2 => m2.edit({ embed: { description: 'a' } })), + m => m.channel.send('x').then(m2 => m2.edit(embed().setDescription('a'))), + m => m.channel.send('x').then(m2 => m2.edit({ embed: embed().setDescription('a') })), + + m => m.channel.send({ embed: { description: 'a' } }).then(m2 => m2.edit({ embed: null })), + m => m.channel.send(embed().setDescription('a')).then(m2 => m2.edit({ embed: null })), + + m => m.channel.send(['x', 'y'], [embed().setDescription('a'), attach(linkB)]), + m => m.channel.send(['x', 'y'], [attach(linkA), attach(linkB)]), + + m => m.channel.send([embed().setDescription('a'), attach(linkB)]), + m => m.channel.send({ + embed: embed().setImage('attachment://two.png'), + files: [attach(linkB, 'two.png')], + }), + m => m.channel.send({ + embed: embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + }), + async m => m.channel.send(['x', 'y', 'z'], { + code: 'js', + embed: embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + files: [{ attachment: await buffer(linkA) }], + }), + + m => m.channel.send('x', attach(fileA)), + m => m.channel.send({ files: [fileA] }), + m => m.channel.send(attach(fileA)), + async m => m.channel.send({ files: [await read(fileA)] }), + async m => m.channel.send(fill('x'), { + reply: m.author, + code: 'js', + split: true, + embed: embed().setImage('attachment://zero.png'), + files: [attach(await buffer(linkA), 'zero.png')], + }), + + m => m.channel.send('x', attach(readStream(fileA))), + m => m.channel.send({ files: [readStream(fileA)] }), + m => m.channel.send({ files: [{ attachment: readStream(fileA) }] }), + async m => m.channel.send(fill('xyz '), { + reply: m.author, + code: 'js', + split: { char: ' ', prepend: 'hello! ', append: '!!!' }, + embed: embed().setImage('attachment://zero.png'), + files: [linkB, attach(await buffer(linkA), 'zero.png'), readStream(fileA)], + }), + + m => m.channel.send('Done!'), +]; + +client.on('message', async message => { + if (message.author.id !== owner) return; + const match = message.content.match(/^do (.+)$/); + if (match && match[1] === 'it') { + /* eslint-disable no-await-in-loop */ + for (const [i, test] of tests.entries()) { + await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + await wait(1000); + } + /* eslint-enable no-await-in-loop */ + } else if (match) { + const n = parseInt(match[1]) || 0; + const test = tests.slice(n)[0]; + const i = tests.indexOf(test); + await message.channel.send(`**#${i}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + } +}); + +client.login(token); + +// eslint-disable-next-line no-console +process.on('unhandledRejection', console.error); diff --git a/typings/index.d.ts b/typings/index.d.ts index 3501f7e9d..a8cf2cf1e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -40,6 +40,33 @@ declare module 'discord.js' { public static FLAGS: Record; } + export class APIMessage { + constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions); + public readonly isUser: boolean; + public readonly isWebhook: boolean; + public options: MessageOptions | WebhookMessageOptions; + public target: MessageTarget; + + public static create( + target: MessageTarget, + content?: StringResolvable, + options?: MessageOptions | WebhookMessageOptions | MessageAdditions, + extra?: MessageOptions | WebhookMessageOptions + ): APIMessage; + public static partitionMessageAdditions(items: (MessageEmbed | MessageAttachment)[]): [MessageEmbed[], MessageAttachment[]]; + public static resolveFile(fileLike: BufferResolvable | Stream | FileOptions | MessageAttachment): Promise; + public static transformOptions( + content: StringResolvable, + options: MessageOptions | WebhookMessageOptions | MessageAdditions, + extra?: MessageOptions | WebhookMessageOptions, + isWebhook?: boolean + ): MessageOptions | WebhookMessageOptions; + + public makeContent(): string | string[]; + public resolveData(): object; + public resolveFiles(): Promise; + } + export class Base { constructor (client: Client); public readonly client: Client; @@ -618,30 +645,29 @@ declare module 'discord.js' { public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; public edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise; + public edit(options: MessageEditOptions | MessageEmbed): Promise; public equals(message: Message, rawData: object): boolean; public fetchWebhook(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; - public reply(content?: StringResolvable, options?: MessageOptions): Promise; - public reply(options?: MessageOptions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + public reply(options?: MessageOptions | MessageAdditions): Promise; public toJSON(): object; public toString(): string; public unpin(): Promise; } export class MessageAttachment { - constructor(file: BufferResolvable | Stream, name?: string); - private _attach(file: BufferResolvable | Stream, name: string): void; + constructor(attachment: BufferResolvable | Stream, name?: string); - public readonly attachment: BufferResolvable | Stream; + public attachment: BufferResolvable | Stream; public height: number; public id: Snowflake; - public readonly name: string; + public name?: string; public proxyURL: string; public url: string; public width: number; - public setAttachment(file: BufferResolvable | Stream, name: string): this; - public setFile(attachment: BufferResolvable | Stream): this; + public setFile(attachment: BufferResolvable | Stream, name?: string): this; public setName(name: string): this; public toJSON(): object; } @@ -1345,8 +1371,8 @@ declare module 'discord.js' { lastMessageID: Snowflake; lastMessageChannelID: Snowflake; readonly lastMessage: Message; - send(content?: StringResolvable, options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; - send(options?: MessageOptions | MessageEmbed | MessageAttachment): Promise; + send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + send(options?: MessageOptions | MessageAdditions): Promise; }; type TextBasedChannelFields = { @@ -1367,8 +1393,8 @@ declare module 'discord.js' { token: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; - send(options?: WebhookMessageOptions | MessageEmbed | MessageAttachment | MessageAttachment[]): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise; + send(options?: WebhookMessageOptions | MessageAdditions): Promise; sendSlackMessage(body: object): Promise; }; @@ -1604,7 +1630,7 @@ declare module 'discord.js' { }; type FileOptions = { - attachment: BufferResolvable; + attachment: BufferResolvable | Stream; name?: string; }; @@ -1783,6 +1809,8 @@ declare module 'discord.js' { maxProcessed?: number; }; + type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; + type MessageEditOptions = { content?: string; embed?: MessageEmbedOptions | null; @@ -1810,7 +1838,7 @@ declare module 'discord.js' { content?: string; embed?: MessageEmbed | MessageEmbedOptions, disableEveryone?: boolean; - files?: (FileOptions | BufferResolvable | MessageAttachment)[]; + files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; reply?: UserResolvable; @@ -1820,6 +1848,8 @@ declare module 'discord.js' { type MessageResolvable = Message | Snowflake; + type MessageTarget = TextChannel | DMChannel | GroupDMChannel | User | GuildMember | Webhook | WebhookClient; + type MessageType = 'DEFAULT' | 'RECIPIENT_ADD' | 'RECIPIENT_REMOVE' @@ -1968,7 +1998,7 @@ declare module 'discord.js' { nonce?: string; embeds?: (MessageEmbed | object)[]; disableEveryone?: boolean; - files?: (FileOptions | BufferResolvable | MessageAttachment)[]; + files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; }; From c59d3299bcbe09ce12cd1b78ed5009a82d80363a Mon Sep 17 00:00:00 2001 From: Isabella Date: Tue, 21 Aug 2018 13:19:45 -0500 Subject: [PATCH 0851/1359] fix: typo in Activity#flags --- src/structures/Presence.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 00cdaa051..0c5cdc191 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -154,7 +154,7 @@ class Activity { /** * Flags that describe the activity - * @type {ReadoOnly} + * @type {ReadOnly} */ this.flags = new ActivityFlags(data.flags).freeze(); } From 928fb300407bf3aea98905708144dc7138035dd9 Mon Sep 17 00:00:00 2001 From: Isabella Date: Tue, 21 Aug 2018 14:25:30 -0500 Subject: [PATCH 0852/1359] fix: actually fix Readonly typos --- src/client/voice/VoiceConnection.js | 8 ++++---- src/structures/GuildChannel.js | 4 ++-- src/structures/GuildMember.js | 4 ++-- src/structures/PermissionOverwrites.js | 4 ++-- src/structures/Presence.js | 2 +- src/structures/Role.js | 4 ++-- src/util/BitField.js | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index eeaf768d0..6e0557995 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -51,7 +51,7 @@ class VoiceConnection extends EventEmitter { /** * Our current speaking state - * @type {ReadOnly} + * @type {Readonly} */ this.speaking = new Speaking().freeze(); @@ -97,7 +97,7 @@ class VoiceConnection extends EventEmitter { /** * Tracks which users are talking - * @type {Map>} + * @type {Map>} * @private */ this._speaking = new Map(); @@ -443,7 +443,7 @@ class VoiceConnection extends EventEmitter { * Emitted whenever a user changes speaking state. * @event VoiceConnection#speaking * @param {User} user The user that has changed speaking state - * @param {ReadOnly} speaking The speaking state of the user + * @param {Readonly} speaking The speaking state of the user */ if (this.status === VoiceStatus.CONNECTED) { this.emit('speaking', user, speaking); @@ -459,7 +459,7 @@ class VoiceConnection extends EventEmitter { * Emitted once a guild member changes speaking state. * @event Client#guildMemberSpeaking * @param {GuildMember} member The member that started/stopped speaking - * @param {ReadOnly} speaking The speaking state of the member + * @param {Readonly} speaking The speaking state of the member */ this.client.emit(Events.GUILD_MEMBER_SPEAKING, member, speaking); } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index b64eca976..20e09093f 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -133,7 +133,7 @@ class GuildChannel extends Channel { /** * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites. * @param {GuildMember} member The member to obtain the overall permissions for - * @returns {ReadOnly} + * @returns {Readonly} * @private */ memberPermissions(member) { @@ -159,7 +159,7 @@ class GuildChannel extends Channel { /** * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. * @param {Role} role The role to obtain the overall permissions for - * @returns {ReadOnly} + * @returns {Readonly} * @private */ rolePermissions(role) { diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index de42e354c..d910117d3 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -168,7 +168,7 @@ class GuildMember extends Base { /** * The overall set of permissions for this member, taking only roles into account - * @type {ReadOnly} + * @type {Readonly} * @readonly */ get permissions() { @@ -209,7 +209,7 @@ class GuildMember extends Base { * Returns `channel.permissionsFor(guildMember)`. Returns permissions for a member in a guild channel, * taking into account roles and permission overwrites. * @param {ChannelResolvable} channel The guild channel to use as context - * @returns {ReadOnly} + * @returns {Readonly} */ permissionsIn(channel) { channel = this.guild.channels.resolve(channel); diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index f2ece7ced..ddf9c4208 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -39,13 +39,13 @@ class PermissionOverwrites { /** * The permissions that are denied for the user or role. - * @type {ReadOnly} + * @type {Readonly} */ this.deny = new Permissions(data.deny).freeze(); /** * The permissions that are allowed for the user or role. - * @type {ReadOnly} + * @type {Readonly} */ this.allow = new Permissions(data.allow).freeze(); } diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 0c5cdc191..339981340 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -154,7 +154,7 @@ class Activity { /** * Flags that describe the activity - * @type {ReadOnly} + * @type {Readonly} */ this.flags = new ActivityFlags(data.flags).freeze(); } diff --git a/src/structures/Role.js b/src/structures/Role.js index e6808a822..18f68f2f9 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -54,7 +54,7 @@ class Role extends Base { /** * The permissions of the role - * @type {ReadOnly} + * @type {Readonly} */ this.permissions = new Permissions(data.permissions).freeze(); @@ -203,7 +203,7 @@ class Role extends Base { * Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel, * taking into account permission overwrites. * @param {ChannelResolvable} channel The guild channel to use as context - * @returns {ReadOnly} + * @returns {Readonly} */ permissionsIn(channel) { channel = this.guild.channels.resolve(channel); diff --git a/src/util/BitField.js b/src/util/BitField.js index 2cd9c907a..fa506a2e8 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -48,7 +48,7 @@ class BitField { /** * Freezes these bits, making them immutable. - * @returns {ReadOnly} These bits + * @returns {Readonly} These bits */ freeze() { return Object.freeze(this); From 02f98cd7e6b04cad51512ccbcd590d055130c70c Mon Sep 17 00:00:00 2001 From: Crawl Date: Wed, 22 Aug 2018 05:05:11 +0200 Subject: [PATCH 0853/1359] fix(webpack): properly minifying --- package.json | 2 +- webpack.config.js | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 9f4e7fec2..026823c22 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "tslint-config-typings": "^0.3.1", "typescript": "^3.0.1", "uglifyjs-webpack-plugin": "^1.3.0", - "webpack": "^4.16.5", + "webpack": "^4.17.0", "webpack-cli": "^3.1.0" }, "engines": { diff --git a/webpack.config.js b/webpack.config.js index c4ec147af..b845d3bd0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,22 +3,8 @@ const webpack = require('webpack'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const version = require('./package.json').version; -const plugins = [ - new webpack.optimize.ModuleConcatenationPlugin(), -]; - const prod = process.env.NODE_ENV === 'production'; -if (prod) { - plugins.push(new UglifyJSPlugin({ - uglifyOptions: { - mangle: { keep_classnames: true }, - compress: { keep_classnames: true }, - output: { comments: false }, - }, - })); -} - // eslint-disable-next-line max-len const filename = `discord${process.env.VERSIONED ? `.${version}` : ''}${prod ? '.min' : ''}.js`; @@ -53,10 +39,23 @@ module.exports = { child_process: 'empty', dgram: 'empty', __dirname: true, - process: false, + process: true, path: 'empty', Buffer: false, zlib: 'empty', }, - plugins, + optimization: { + minimizer: [ + new UglifyJSPlugin({ + uglifyOptions: { + mangle: { keep_classnames: true }, + compress: { keep_classnames: true }, + output: { comments: false }, + }, + }), + ], + }, + plugins: [ + new webpack.optimize.ModuleConcatenationPlugin(), + ], }; From 28d4f74b6570706edc769d7698e3925dfad1ead1 Mon Sep 17 00:00:00 2001 From: Kyra Date: Wed, 22 Aug 2018 08:10:55 +0200 Subject: [PATCH 0854/1359] misc: update ClientApplication for the current API (#2767) * misc: Update ClientApplication for the current API * cleanup: ClientApplication#fetchAssets, removed createAsset * Major cleanup time * Merge to kyra's branch * docs: Updated typings * fix: re-add ClientApplication#{cover,coverImage()} * typings(ClientApplication): move coverImage declaration up --- src/structures/ClientApplication.js | 79 ++++++++--------------------- typings/index.d.ts | 7 +-- 2 files changed, 21 insertions(+), 65 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 59dc82671..b08648bdc 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -1,8 +1,9 @@ const Snowflake = require('../util/Snowflake'); const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants'); -const DataResolver = require('../util/DataResolver'); const Base = require('./Base'); +const AssetTypes = Object.keys(ClientApplicationAssetTypes); + /** * Represents a Client OAuth2 Application. * @extends {Base} @@ -39,22 +40,16 @@ class ClientApplication extends Base { this.icon = data.icon; /** - * The app's cover image hash + * The app's cover image * @type {?string} */ - this.cover = data.cover_image; + this.cover = data.cover_image || null; /** - * The app's RPC origins + * The app's RPC origins, if enabled * @type {?string[]} */ - this.rpcOrigins = data.rpc_origins; - - /** - * The app's redirect URIs - * @type {string[]} - */ - this.redirectURIs = data.redirect_uris; + this.rpcOrigins = data.rpc_origins || []; /** * If this app's bot requires a code grant when using the OAuth2 flow @@ -69,36 +64,10 @@ class ClientApplication extends Base { this.botPublic = data.bot_public; /** - * If this app can use rpc - * @type {boolean} + * The owner of this OAuth application + * @type {User} */ - this.rpcApplicationState = data.rpc_application_state; - - /** - * Object containing basic info about this app's bot - * @type {Object} - */ - this.bot = data.bot; - - /** - * The flags for the app - * @type {number} - */ - this.flags = data.flags; - - /** - * OAuth2 secret for the application - * @type {string} - */ - this.secret = data.secret; - - if (data.owner) { - /** - * The owner of this OAuth application - * @type {?User} - */ - this.owner = this.client.users.add(data.owner); - } + this.owner = this.client.users.add(data.owner); } /** @@ -142,34 +111,26 @@ class ClientApplication extends Base { } /** - * Get rich presence assets. - * @returns {Promise} + * Asset data. + * @typedef {Object} ClientAsset + * @property {Snowflake} id The asset ID + * @property {string} name The asset name + * @property {string} type The asset type + */ + + /** + * Gets the clients rich presence assets. + * @returns {Promise>} */ fetchAssets() { - 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: types[a.type - 1], + type: AssetTypes[a.type - 1], }))); } - /** - * Creates a rich presence asset. - * @param {string} name Name of the asset - * @param {Base64Resolvable} data Data of the asset - * @param {string} type Type of the asset. `big`, or `small` - * @returns {Promise} - */ - async createAsset(name, data, type) { - return this.client.api.oauth2.applications(this.id).assets.post({ data: { - name, - type: ClientApplicationAssetTypes[type.toUpperCase()], - image: await DataResolver.resolveImage(data), - } }); - } - /** * When concatenated with a string, this automatically returns the application's name instead of the * ClientApplication object. diff --git a/typings/index.d.ts b/typings/index.d.ts index a8cf2cf1e..449c8077d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -228,10 +228,9 @@ declare module 'discord.js' { export class ClientApplication extends Base { constructor(client: Client, data: object); - public bot: object; public botPublic: boolean; public botRequireCodeGrant: boolean; - public cover: string; + public cover?: string; public readonly createdAt: Date; public readonly createdTimestamp: number; public description: string; @@ -239,12 +238,8 @@ declare module 'discord.js' { public id: Snowflake; public name: string; public owner: User; - public redirectURIs: string[]; - public rpcApplicationState: boolean; public rpcOrigins: string[]; - public secret: string; public coverImage(options?: AvatarOptions): string; - public createAsset(name: string, data: Base64Resolvable, type: 'big' | 'small' | 'Big' | 'Small'): Promise; public fetchAssets(): Promise; public iconURL(options?: AvatarOptions): string; public toJSON(): object; From 82993fbe510aa3f170708659fc39a3b25d07261b Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 22 Aug 2018 19:26:57 +0200 Subject: [PATCH 0855/1359] fix(ClientApplication): Message#application is a partial ClientApplication --- src/structures/ClientApplication.js | 14 +++++++------- typings/index.d.ts | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index b08648bdc..7ea360200 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -47,27 +47,27 @@ class ClientApplication extends Base { /** * The app's RPC origins, if enabled - * @type {?string[]} + * @type {string[]} */ this.rpcOrigins = data.rpc_origins || []; /** * If this app's bot requires a code grant when using the OAuth2 flow - * @type {boolean} + * @type {?boolean} */ - this.botRequireCodeGrant = data.bot_require_code_grant; + this.botRequireCodeGrant = typeof data.bot_require_code_grant !== 'undefined' ? data.bot_require_code_grant : null; /** * If this app's bot is public - * @type {boolean} + * @type {?boolean} */ - this.botPublic = data.bot_public; + this.botPublic = typeof data.bot_public !== 'undefined' ? data.bot_public : null; /** * The owner of this OAuth application - * @type {User} + * @type {?User} */ - this.owner = this.client.users.add(data.owner); + this.owner = data.owner ? this.client.users.add(data.owner) : null; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 449c8077d..a0d4ec83d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -228,8 +228,8 @@ declare module 'discord.js' { export class ClientApplication extends Base { constructor(client: Client, data: object); - public botPublic: boolean; - public botRequireCodeGrant: boolean; + public botPublic?: boolean; + public botRequireCodeGrant?: boolean; public cover?: string; public readonly createdAt: Date; public readonly createdTimestamp: number; @@ -237,7 +237,7 @@ declare module 'discord.js' { public icon: string; public id: Snowflake; public name: string; - public owner: User; + public owner?: User; public rpcOrigins: string[]; public coverImage(options?: AvatarOptions): string; public fetchAssets(): Promise; From 13f46b924be15371f377d3c5f128ac75ac98e4b4 Mon Sep 17 00:00:00 2001 From: Isabella Date: Thu, 23 Aug 2018 23:29:44 -0500 Subject: [PATCH 0856/1359] refactor: clean up rate limit handling (#2694) * refactor: clean up rate limit handling * requested changes * remove request mode option * fix dupe requests * hardcode reaction ratelimits * suggested changes * fix small thing * re-add restTimeOffset * move restTimeOffset a bit * i swear i know english its my native language ok * requested changes * fix: a bit *too* pre-emptive with ratelimits, now less so * fix: dapi error shoudl reject with path * fix: make errors in execute catchable * fix promise return * rebase is hard --- src/rest/HTTPError.js | 31 +++++ src/rest/RESTManager.js | 15 +-- src/rest/RequestHandler.js | 180 ++++++++++++++++++++++++++++ src/rest/handlers/RequestHandler.js | 124 ------------------- src/rest/handlers/burst.js | 15 --- src/rest/handlers/index.js | 5 - src/rest/handlers/sequential.js | 18 --- src/util/Constants.js | 2 - 8 files changed, 215 insertions(+), 175 deletions(-) create mode 100644 src/rest/HTTPError.js create mode 100644 src/rest/RequestHandler.js delete mode 100644 src/rest/handlers/RequestHandler.js delete mode 100644 src/rest/handlers/burst.js delete mode 100644 src/rest/handlers/index.js delete mode 100644 src/rest/handlers/sequential.js diff --git a/src/rest/HTTPError.js b/src/rest/HTTPError.js new file mode 100644 index 000000000..293be7c5c --- /dev/null +++ b/src/rest/HTTPError.js @@ -0,0 +1,31 @@ +class HTTPError extends Error { + constructor(message, name, code, method, path) { + super(message); + + /** + * The name of the error + * @type {string} + */ + this.name = name; + + /** + * HTTP error code returned from the request + * @type {number} + */ + this.code = code || 500; + + /** + * The HTTP method used for the request + * @type {string} + */ + this.method = method; + + /** + * The path of the request relative to the HTTP endpoint + * @type {string} + */ + this.path = path; + } +} + +module.exports = HTTPError; diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 1d2f66e5e..4b725f4d2 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -1,4 +1,4 @@ -const handlers = require('./handlers'); +const RequestHandler = require('./RequestHandler'); const APIRequest = require('./APIRequest'); const routeBuilder = require('./APIRouter'); const { Error } = require('../errors'); @@ -12,6 +12,7 @@ class RESTManager { this.globallyRateLimited = false; this.tokenPrefix = tokenPrefix; this.versioned = true; + this.globalTimeout = null; if (client.options.restSweepInterval > 0) { client.setInterval(() => { this.handlers.sweep(handler => handler._inactive); @@ -41,24 +42,16 @@ class RESTManager { request: apiRequest, resolve, reject, - }); + }).catch(reject); }); } - getRequestHandler() { - const method = this.client.options.apiRequestMethod; - if (typeof method === 'function') return method; - const handler = handlers[method]; - if (!handler) throw new Error('RATELIMIT_INVALID_METHOD'); - return handler; - } - request(method, url, options = {}) { const apiRequest = new APIRequest(this, method, url, options); let handler = this.handlers.get(apiRequest.route); if (!handler) { - handler = new handlers.RequestHandler(this, this.getRequestHandler()); + handler = new RequestHandler(this); this.handlers.set(apiRequest.route, handler); } diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js new file mode 100644 index 000000000..1e3128bd0 --- /dev/null +++ b/src/rest/RequestHandler.js @@ -0,0 +1,180 @@ +const DiscordAPIError = require('./DiscordAPIError'); +const HTTPError = require('./HTTPError'); +const Util = require('../util/Util'); +const { Events: { RATE_LIMIT }, browser } = require('../util/Constants'); + +function parseResponse(res) { + if (res.headers.get('content-type').startsWith('application/json')) return res.json(); + if (browser) return res.blob(); + return res.buffer(); +} + +function getAPIOffset(serverDate) { + return new Date(serverDate).getTime() - Date.now(); +} + +function calculateReset(reset, serverDate) { + return new Date(Number(reset) * 1000).getTime() - getAPIOffset(serverDate); +} + +class RequestHandler { + constructor(manager) { + this.manager = manager; + this.busy = false; + this.queue = []; + this.reset = -1; + this.remaining = -1; + this.limit = -1; + this.retryAfter = -1; + } + + push(request) { + if (this.busy) { + this.queue.push(request); + return this.run(); + } else { + return this.execute(request); + } + } + + run() { + if (this.queue.length === 0) return Promise.resolve(); + return this.execute(this.queue.shift()); + } + + get limited() { + return (this.manager.globallyRateLimited || this.remaining <= 0) && Date.now() < this.reset; + } + + get _inactive() { + return this.queue.length === 0 && !this.limited && this.busy !== true; + } + + /* eslint-disable-next-line complexity */ + async execute(item) { + // Insert item back to the beginning if currently busy + if (this.busy) { + this.queue.unshift(item); + return null; + } + + this.busy = true; + const { reject, request, resolve } = item; + + // After calculations and requests have been done, pre-emptively stop further requests + if (this.limited) { + const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now(); + + if (this.manager.client.listenerCount(RATE_LIMIT)) { + /** + * Emitted when the client hits a rate limit while making a request + * @event Client#rateLimit + * @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 {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.manager.client.emit(RATE_LIMIT, { + timeout, + limit: this.limit, + method: request.method, + path: request.path, + route: request.route, + }); + } + + if (this.manager.globallyRateLimited && !this.manager.globalTimeout) { + // Set a global rate limit for all of the handlers instead of each one individually + this.manager.globalTimeout = this.manager.client.setTimeout(() => { + this.manager.globalTimeout = null; + this.manager.globallyRateLimited = false; + this.busy = false; + this.run(); + }, timeout); + } else if (this.manager.globalTimeout) { + // Already waiting for a global rate limit to clear + this.queue.unshift(item); + return null; + } else { + // Wait for the timeout to expire in order to avoid an actual 429 + await Util.delayFor(timeout); + } + } + + // Perform the request + let res; + try { + res = await request.make(); + } catch (error) { + // NodeFetch error expected for all "operational" errors, such as 500 status code + this.busy = false; + return reject( + new HTTPError(error.message, error.constructor.name, error.status, request.method, request.route) + ); + } + + if (res && res.headers) { + if (res.headers.get('x-ratelimit-global')) this.manager.globallyRateLimited = true; + + const serverDate = res.headers.get('date'); + const limit = res.headers.get('x-ratelimit-limit'); + const remaining = res.headers.get('x-ratelimit-remaining'); + const reset = res.headers.get('x-ratelimit-reset'); + const retryAfter = res.headers.get('retry-after'); + + this.limit = limit ? Number(limit) : Infinity; + this.remaining = remaining ? Number(remaining) : 1; + this.reset = reset ? calculateReset(reset, serverDate) : Date.now(); + this.retryAfter = retryAfter ? Number(retryAfter) : -1; + + // https://github.com/discordapp/discord-api-docs/issues/182 + if (item.request.route.includes('reactions')) { + this.reset = Date.now() + getAPIOffset(serverDate) + 250; + } + } + + // Finished handling headers, safe to unlock manager + this.busy = false; + + if (res.ok) { + const success = await parseResponse(res); + // Nothing wrong with the request, proceed with the next + resolve(success); + return this.run(); + } else if (res.status === 429) { + // A ratelimit was hit - this should never happen + this.queue.unshift(item); + this.manager.client.emit('debug', `429 hit on route ${item.request.route}`); + await Util.delayFor(this.retryAfter); + return this.run(); + } else if (res.status >= 500 && res.status < 600) { + // Retry once for possible serverside issues + if (item.retried) { + return reject( + new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.route) + ); + } else { + item.retried = true; + this.queue.unshift(item); + return this.run(); + } + } else { + // Handle possible malformed requessts + try { + const data = await parseResponse(res); + if (res.status >= 400 && res.status < 500) { + return reject(new DiscordAPIError(request.path, data, request.method)); + } + return null; + } catch (err) { + return reject( + new HTTPError(err.message, err.constructor.name, err.status, request.method, request.route) + ); + } + } + } +} + +module.exports = RequestHandler; diff --git a/src/rest/handlers/RequestHandler.js b/src/rest/handlers/RequestHandler.js deleted file mode 100644 index 464c71781..000000000 --- a/src/rest/handlers/RequestHandler.js +++ /dev/null @@ -1,124 +0,0 @@ -const DiscordAPIError = require('../DiscordAPIError'); -const { Events: { RATE_LIMIT }, browser } = require('../../util/Constants'); - -function parseResponse(res) { - if (res.headers.get('content-type').startsWith('application/json')) return res.json(); - if (browser) return res.blob(); - return res.buffer(); -} - -class RequestHandler { - constructor(manager, handler) { - this.manager = manager; - this.client = this.manager.client; - this.handle = handler.bind(this); - this.limit = Infinity; - this.resetTime = null; - this.remaining = 1; - this.busy = false; - this.queue = []; - } - - get limited() { - return (this.manager.globallyRateLimited || this.remaining <= 0) && Date.now() < this.resetTime; - } - - push(request) { - this.queue.push(request); - this.handle(); - } - - get _inactive() { - return this.queue.length === 0 && !this.limited && this.busy !== true; - } - - /* eslint-disable prefer-promise-reject-errors */ - execute(item) { - return new Promise((resolve, reject) => { - const finish = timeout => { - if (timeout || this.limited) { - if (!timeout) { - timeout = this.resetTime - Date.now() + this.client.options.restTimeOffset; - } - if (!this.manager.globalTimeout && this.manager.globallyRateLimited) { - this.manager.globalTimeout = setTimeout(() => { - this.manager.globalTimeout = undefined; - this.manager.globallyRateLimited = false; - this.busy = false; - this.handle(); - }, timeout); - reject({ }); - } else { - reject({ timeout }); - } - if (this.client.listenerCount(RATE_LIMIT)) { - /** - * Emitted when the client hits a rate limit while making a request - * @event Client#rateLimit - * @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 {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, - limit: this.limit, - method: item.request.method, - path: item.request.path, - route: item.request.route, - }); - } - } else { - resolve(); - } - }; - item.request.make().then(res => { - if (res && res.headers) { - if (res.headers.get('x-ratelimit-global')) this.manager.globallyRateLimited = true; - this.limit = Number(res.headers.get('x-ratelimit-limit') || Infinity); - const reset = res.headers.get('x-ratelimit-reset'); - this.resetTime = reset !== null ? - (Number(reset) * 1e3) - new Date(res.headers.get('date') || Date.now()).getTime() + Date.now() : - Date.now(); - const remaining = res.headers.get('x-ratelimit-remaining'); - this.remaining = remaining !== null ? Number(remaining) : 1; - } - - if (res.ok) { - parseResponse(res).then(item.resolve, item.reject); - finish(); - return; - } - - if (res.status === 429) { - this.queue.unshift(item); - finish(Number(res.headers.get('retry-after')) + this.client.options.restTimeOffset); - } else if (res.status >= 500 && res.status < 600) { - if (item.retried) { - item.reject(res); - finish(); - } else { - item.retried = true; - this.queue.unshift(item); - finish(1e3 + this.client.options.restTimeOffset); - } - } else { - parseResponse(res).then(data => { - item.reject(res.status >= 400 && res.status < 500 ? - new DiscordAPIError(item.request.path, data, item.request.method) : res); - }, item.reject); - finish(); - } - }); - }); - } - - reset() { - this.manager.globallyRateLimited = false; - this.remaining = 1; - } -} - -module.exports = RequestHandler; diff --git a/src/rest/handlers/burst.js b/src/rest/handlers/burst.js deleted file mode 100644 index 54e4600ff..000000000 --- a/src/rest/handlers/burst.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = function burst() { - if (this.limited || this.queue.length === 0) return; - this.execute(this.queue.shift()) - .then(this.handle.bind(this)) - .catch(({ timeout }) => { - if (timeout) { - this.client.setTimeout(() => { - this.reset(); - this.handle(); - }, timeout); - } - }); - this.remaining--; - this.handle(); -}; diff --git a/src/rest/handlers/index.js b/src/rest/handlers/index.js deleted file mode 100644 index 47792c46c..000000000 --- a/src/rest/handlers/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - sequential: require('./sequential'), - burst: require('./burst'), - RequestHandler: require('./RequestHandler'), -}; diff --git a/src/rest/handlers/sequential.js b/src/rest/handlers/sequential.js deleted file mode 100644 index 71020282d..000000000 --- a/src/rest/handlers/sequential.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = function sequential() { - if (this.busy || this.limited || this.queue.length === 0) return; - this.busy = true; - this.execute(this.queue.shift()) - .then(() => { - this.busy = false; - this.handle(); - }) - .catch(({ timeout }) => { - if (timeout) { - this.client.setTimeout(() => { - this.reset(); - this.busy = false; - this.handle(); - }, timeout); - } - }); -}; diff --git a/src/util/Constants.js b/src/util/Constants.js index 3e1b317d4..1bc05ef78 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -5,7 +5,6 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {string} [apiRequestMethod='sequential'] One of `sequential` or `burst`. The sequential handler executes * all requests in the order they are triggered, whereas the burst handler runs multiple in parallel, and doesn't * provide the guarantee of any particular order. Burst mode is more likely to hit a 429 ratelimit error by its nature, * and is therefore slightly riskier to use. @@ -36,7 +35,6 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {HTTPOptions} [http] HTTP options */ exports.DefaultOptions = { - apiRequestMethod: 'sequential', shardId: 0, shardCount: 0, internalSharding: false, From c4df02782efadbfa152a697d6085191efb54d272 Mon Sep 17 00:00:00 2001 From: Kyra Date: Fri, 24 Aug 2018 16:03:29 +0200 Subject: [PATCH 0857/1359] feat: Guild Integrations (#2760) * feat: Guild Integrations * feat: Guild#createIntegration * feat: Add ws event GUILD_INTEGRATIONS_UPDATE * docs: Add `GUILD_INTEGRATIONS_UPDATE` to WSEventType * misc: Fixed requested change * docs: Updated typings * typings: Sort members by name --- .../packets/WebSocketPacketManager.js | 1 + .../handlers/GuildIntegrationsUpdate.js | 19 +++ src/index.js | 1 + src/structures/Guild.js | 37 +++++ src/structures/Integration.js | 153 ++++++++++++++++++ src/util/Constants.js | 3 + typings/index.d.ts | 39 +++++ 7 files changed, 253 insertions(+) create mode 100644 src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js create mode 100644 src/structures/Integration.js diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js index 06fcae12f..a58871380 100644 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ b/src/client/websocket/packets/WebSocketPacketManager.js @@ -31,6 +31,7 @@ class WebSocketPacketManager { this.register(WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate')); this.register(WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate')); this.register(WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk')); + this.register(WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate')); this.register(WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate')); this.register(WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete')); this.register(WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate')); diff --git a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js b/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js new file mode 100644 index 000000000..5adfb5b0f --- /dev/null +++ b/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js @@ -0,0 +1,19 @@ +const AbstractHandler = require('./AbstractHandler'); +const { Events } = require('../../../../util/Constants'); + +class GuildIntegrationsHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + const data = packet.d; + const guild = client.guilds.get(data.guild_id); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsHandler; + +/** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ diff --git a/src/index.js b/src/index.js index 1c9f068ce..a124108f6 100644 --- a/src/index.js +++ b/src/index.js @@ -68,6 +68,7 @@ module.exports = { GuildChannel: require('./structures/GuildChannel'), GuildEmoji: require('./structures/GuildEmoji'), GuildMember: require('./structures/GuildMember'), + Integration: require('./structures/Integration'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), MessageAttachment: require('./structures/MessageAttachment'), diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a698c1049..d24b3c175 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1,4 +1,5 @@ const Invite = require('./Invite'); +const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); @@ -407,6 +408,42 @@ class Guild extends Base { ); } + /** + * Fetches a collection of integrations to this guild. + * Resolves with a collection mapping integrations by their ids. + * @returns {Promise>} + * @example + * // Fetch integrations + * guild.fetchIntegrations() + * .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) + * .catch(console.error); + */ + fetchIntegrations() { + return this.client.api.guilds(this.id).integrations.get().then(data => + data.reduce((collection, integration) => + collection.set(integration.id, new Integration(this.client, integration, this)), + new Collection()) + ); + } + + /** + * The data for creating an integration. + * @typedef {Object} IntegrationData + * @property {string} id The integration id + * @property {string} type The integration type + */ + + /** + * Creates an integration by attaching an integration object + * @param {IntegrationData} data The data for thes integration + * @param {string} reason Reason for creating the integration + * @returns {Promise} + */ + createIntegration(data, reason) { + return this.client.api.guilds(this.id).integrations.post({ data, reason }) + .then(() => this); + } + /** * Fetches a collection of invites to this guild. * Resolves with a collection mapping invites by their codes. diff --git a/src/structures/Integration.js b/src/structures/Integration.js new file mode 100644 index 000000000..359df9eb3 --- /dev/null +++ b/src/structures/Integration.js @@ -0,0 +1,153 @@ +const Base = require('./Base'); + +/** + * The information account for an integration + * @typedef {Object} IntegrationAccount + * @property {string} id The id of the account + * @property {string} name The name of the account + */ + +class Integration extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The guild this integration belongs to + * @type {Guild} + */ + this.guild = guild; + + /** + * The integration id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The integration name + * @type {string} + */ + this.name = data.name; + + /** + * The integration type (twitch, youtube, etc) + * @type {string} + */ + this.type = data.type; + + /** + * Whether this integration is enabled + * @type {boolean} + */ + this.enabled = data.enabled; + + /** + * Whether this integration is syncing + * @type {boolean} + */ + this.syncing = data.syncing; + + /** + * The role that this integration uses for subscribers + * @type {Role} + */ + this.role = this.guild.roles.get(data.role_id); + + /** + * The user for this integration + * @type {User} + */ + this.user = this.client.users.add(data.user); + + /** + * The account integration information + * @type {IntegrationAccount} + */ + this.account = data.account; + + /** + * The last time this integration was last synced + * @type {number} + */ + this.syncedAt = data.synced_at; + this._patch(data); + } + + _patch(data) { + /** + * The behavior of expiring subscribers + * @type {number} + */ + this.expireBehavior = data.expire_behavior; + + /** + * The grace period before expiring subscribers + * @type {number} + */ + this.expireGracePeriod = data.expire_grace_period; + } + + /** + * Sync this integration + * @returns {Promise} + */ + sync() { + this.syncing = true; + return this.client.api.guilds(this.guild.id).integrations(this.id).post() + .then(() => { + this.syncing = false; + this.syncedAt = Date.now(); + return this; + }); + } + + /** + * The data for editing an integration. + * @typedef {Object} IntegrationEditData + * @property {number} [expireBehavior] The new behaviour of expiring subscribers + * @property {number} [expireGracePeriod] The new grace period before expiring subscribers + */ + + /** + * Edits this integration. + * @param {IntegrationEditData} data The data to edit this integration with + * @param {string} reason Reason for editing this integration + * @returns {Promise} + */ + edit(data, reason) { + if ('expireBehavior' in data) { + data.expire_behavior = data.expireBehavior; + data.expireBehavior = null; + } + if ('expireGracePeriod' in data) { + data.expire_grace_period = data.expireGracePeriod; + data.expireGracePeriod = null; + } + // The option enable_emoticons is only available for Twitch at this moment + return this.client.api.guilds(this.guild.id).integrations(this.id).patch({ data, reason }) + .then(() => { + this._patch(data); + return this; + }); + } + + /** + * Deletes this integration. + * @returns {Promise} + * @param {string} [reason] Reason for deleting this integration + */ + delete(reason) { + return this.client.api.guilds(this.guild.id).integrations(this.id).delete({ reason }) + .then(() => this); + } + + toJSON() { + return super.toJSON({ + role: 'roleID', + guild: 'guildID', + user: 'userID', + }); + } +} + +module.exports = Integration; diff --git a/src/util/Constants.js b/src/util/Constants.js index 1bc05ef78..7f5e7429b 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -219,6 +219,7 @@ exports.Events = { GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable', GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking', GUILD_MEMBERS_CHUNK: 'guildMembersChunk', + GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', GUILD_ROLE_CREATE: 'roleCreate', GUILD_ROLE_DELETE: 'roleDelete', GUILD_ROLE_UPDATE: 'roleUpdate', @@ -266,6 +267,7 @@ exports.Events = { * * GUILD_MEMBER_REMOVE * * GUILD_MEMBER_UPDATE * * GUILD_MEMBERS_CHUNK + * * GUILD_INTEGRATIONS_UPDATE * * GUILD_ROLE_CREATE * * GUILD_ROLE_DELETE * * GUILD_ROLE_UPDATE @@ -302,6 +304,7 @@ exports.WSEvents = keyMirror([ 'GUILD_MEMBER_REMOVE', 'GUILD_MEMBER_UPDATE', 'GUILD_MEMBERS_CHUNK', + 'GUILD_INTEGRATIONS_UPDATE', 'GUILD_ROLE_CREATE', 'GUILD_ROLE_DELETE', 'GUILD_ROLE_UPDATE', diff --git a/typings/index.d.ts b/typings/index.d.ts index a0d4ec83d..244555d9e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -178,6 +178,7 @@ declare module 'discord.js' { public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; @@ -209,6 +210,7 @@ declare module 'discord.js' { public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; @@ -431,11 +433,13 @@ declare module 'discord.js' { public readonly verified: boolean; public readonly voiceConnection: VoiceConnection; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; + public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; public equals(guild: Guild): boolean; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchBans(): Promise>; + public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; @@ -578,6 +582,25 @@ declare module 'discord.js' { public toString(): string; } + export class Integration extends Base { + constructor(client: Client, data: object, guild: Guild); + public account: IntegrationAccount; + public enabled: boolean; + public expireBehavior: number; + public expireGracePeriod: number; + public guild: Guild; + public id: Snowflake; + public name: string; + public role: Role; + public syncedAt: number; + public syncing: boolean; + public type: number; + public user: User; + public delete(reason?: string): Promise; + public edit(data: IntegrationEditData, reason?: string): Promise; + public sync(): Promise; + } + export class Invite extends Base { constructor(client: Client, data: object); public channel: GuildChannel | GroupDMChannel; @@ -1789,6 +1812,21 @@ declare module 'discord.js' { | 1024 | 2048; + type IntegrationData = { + id: string; + type: string; + }; + + type IntegrationEditData = { + expireBehavior?: number; + expireGracePeriod?: number; + }; + + type IntegrationAccount = { + id: string; + name: string; + }; + type InviteOptions = { temporary?: boolean; maxAge?: number; @@ -2018,6 +2056,7 @@ declare module 'discord.js' { | 'GUILD_BAN_ADD' | 'GUILD_BAN_REMOVE' | 'GUILD_EMOJIS_UPDATE' + | 'GUILD_INTEGRATIONS_UPDATE' | 'CHANNEL_CREATE' | 'CHANNEL_DELETE' | 'CHANNEL_UPDATE' From cb08a0961ac07099d0efdd58e7f0a1ca9f535635 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 24 Aug 2018 16:51:15 +0200 Subject: [PATCH 0858/1359] Docs: merge PermissionOverwriteOptions and OverwriteData (#2738) * Docs: merge PermissionOverwriteOptions and OverwriteData * fix user-member and replace eslint-disable with linebreaks * consistency fix * Fix: remove empty line between jsdocs and method * Fix: allowed/denied to allow/deny to conform with recent master changes --- src/stores/GuildChannelStore.js | 11 ++--------- src/structures/GuildChannel.js | 18 +++++++++--------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index e73b52e98..d775622d7 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -21,14 +21,6 @@ class GuildChannelStore extends DataStore { return Channel.create(this.client, data, this.guild); } - /** - * Can be used to overwrite permissions when creating a channel. - * @typedef {Object} PermissionOverwriteOptions - * @property {PermissionResolvable} [allow] The permissions to allow - * @property {PermissionResolvable} [deny] The permissions to deny - * @property {RoleResolvable|UserResolvable} id ID of the role or member this overwrite is for - */ - /** * Creates a new channel in the guild. * @param {string} name The name of the new channel @@ -39,7 +31,8 @@ class GuildChannelStore extends DataStore { * @param {number} [options.bitrate] Bitrate of the new channel in bits (only voice) * @param {number} [options.userLimit] Maximum amount of users allowed in the new channel (only voice) * @param {ChannelResolvable} [options.parent] Parent of the new channel - * @param {Array} [options.overwrites] Permission overwrites + * @param {OverwriteData[]|Collection} [options.overwrites] + * Permission overwrites of the new channel * @param {string} [options.reason] Reason for creating the channel * @returns {Promise} * @example diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 20e09093f..ffae8c36b 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -176,11 +176,11 @@ class GuildChannel extends Channel { .freeze(); } - /* eslint-disable max-len */ /** * Replaces the permission overwrites in this channel. * @param {Object} [options] Options - * @param {Array|Collection} [options.overwrites] Permission overwrites + * @param {OverwriteData[]|Collection} [options.overwrites] + * Permission overwrites the channel gets updated with * @param {string} [options.reason] Reason for updating the channel overwrites * @returns {Promise} * @example @@ -198,7 +198,6 @@ class GuildChannel extends Channel { return this.edit({ permissionOverwrites: resolvePermissions.call(this, overwrites), reason }) .then(() => this); } - /* eslint-enable max-len */ /** * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). @@ -307,17 +306,18 @@ class GuildChannel extends Channel { * @property {number} [bitrate] The bitrate of the voice channel * @property {number} [userLimit] The user limit of the voice channel * @property {Snowflake} [parentID] The parent ID of the channel - * @property {boolean} [lockPermissions] Lock the permissions of the channel to what the parent's permissions are - * @property {OverwriteData[]} [permissionOverwrites] An array of overwrites to set for the channel + * @property {boolean} [lockPermissions] + * Lock the permissions of the channel to what the parent's permissions are + * @property {OverwriteData[]|Collection} [permissionOverwrites] + * Permission overwrites for the channel */ /** * The data for a permission overwrite * @typedef {Object} OverwriteData - * @property {string} id The id of the overwrite - * @property {string} type The type of the overwrite, either role or member - * @property {number} allow The bitfield for the allowed permissions - * @property {number} deny The bitfield for the denied permissions + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny + * @property {GuildMemberResolvable|RoleResolvable} memberOrRole Member or role this overwrite is for */ /** From 9b329a457c90eae43496f45f596005c8eb9fa662 Mon Sep 17 00:00:00 2001 From: Crawl Date: Fri, 24 Aug 2018 16:51:33 +0200 Subject: [PATCH 0859/1359] build(peer-deps): use uws fork (#2781) * feat: use uws fork * chore: update to 149 for uws --- README.md | 2 +- docs/general/welcome.md | 2 +- package.json | 3 ++- src/WebSocket.js | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d3c2a5cc8..c4cb7c5ec 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ For production bots, using node-opus should be considered a necessity, especiall - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) -- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws`) +- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`) - [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`) ## Example usage diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 730134184..eb94f475a 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -51,7 +51,7 @@ For production bots, using node-opus should be considered a necessity, especiall - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) -- [uws](https://www.npmjs.com/package/uws) for a much faster WebSocket connection (`npm install uws`) +- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`) - [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`) ## Example usage diff --git a/package.json b/package.json index 026823c22..9f33875bd 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "erlpack": "discordapp/erlpack", "libsodium-wrappers": "^0.7.3", "sodium": "^2.0.3", - "uws": "<=10.148.1", + "@discordjs/uws": "^10.149.0", "zlib-sync": "^0.1.4" }, "devDependencies": { @@ -69,6 +69,7 @@ "https": false, "ws": false, "uws": false, + "@discordjs/uws": false, "erlpack": false, "prism-media": false, "opusscript": false, diff --git a/src/WebSocket.js b/src/WebSocket.js index 339e76b98..c6f4209fb 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -9,7 +9,7 @@ if (browser) { exports.WebSocket = window.WebSocket; // eslint-disable-line no-undef } else { try { - exports.WebSocket = require('uws'); + exports.WebSocket = require('@discordjs/uws'); } catch (err) { exports.WebSocket = require('ws'); } From 6f62d7d8166bcb17d44ec1128699316b9224a1dc Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 25 Aug 2018 19:41:07 +0100 Subject: [PATCH 0860/1359] fix: voice not throwing errors on bad stream input (#2786) --- src/client/voice/util/PlayInterface.js | 28 ++++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 2a3712f49..58d31141e 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -63,19 +63,21 @@ class PlayInterface { if (!this.player.playBroadcast) throw new Error('VOICE_PLAY_INTERFACE_NO_BROADCAST'); return this.player.playBroadcast(resource, options); } - const type = options.type || 'unknown'; - if (type === 'unknown') { - return this.player.playUnknown(resource, options); - } else if (type === 'converted') { - return this.player.playPCMStream(resource, options); - } else if (type === 'opus') { - return this.player.playOpusStream(resource, options); - } else if (type === 'ogg/opus') { - if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); - return this.player.playOpusStream(resource.pipe(new prism.OggOpusDemuxer()), options); - } else if (type === 'webm/opus') { - if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); - return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer()), options); + if (resource instanceof Readable || typeof resource === 'string') { + const type = options.type || 'unknown'; + if (type === 'unknown') { + return this.player.playUnknown(resource, options); + } else if (type === 'converted') { + return this.player.playPCMStream(resource, options); + } else if (type === 'opus') { + return this.player.playOpusStream(resource, options); + } else if (type === 'ogg/opus') { + if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); + return this.player.playOpusStream(resource.pipe(new prism.OggOpusDemuxer()), options); + } else if (type === 'webm/opus') { + if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); + return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer()), options); + } } throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); } From 9c2aeb733a35c81f2d1b6261bea430cb9eeb6c13 Mon Sep 17 00:00:00 2001 From: lipgloss Date: Sun, 26 Aug 2018 12:06:00 -0600 Subject: [PATCH 0861/1359] fix: private_channels always returns an empty array on ready (#2787) --- src/client/websocket/packets/handlers/Ready.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js index 0e16400d1..5b14c8339 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -17,7 +17,6 @@ class ReadyHandler extends AbstractHandler { client.users.set(clientUser.id, clientUser); for (const guild of data.guilds) client.guilds.add(guild); - for (const privateDM of data.private_channels) client.channels.add(privateDM); const t = client.setTimeout(() => { client.ws.connection.triggerReady(); From f326fe67b1dec620c718e08ad8028abd0a577620 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 28 Aug 2018 18:25:38 -0500 Subject: [PATCH 0862/1359] fix: reactions ratelimits (#2795) * each reaction doesn't have it's own ratelimit * fix hard-coded reset for reacting --- src/rest/APIRouter.js | 1 + src/rest/RequestHandler.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 830c8501b..678f2fe70 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -22,6 +22,7 @@ function buildRoute(manager) { versioned: manager.versioned, route: route.map((r, i) => { if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id'; + if (route[i - 1] === 'reactions') return ':reaction'; return r; }).join('/'), }, options)).catch(error => { diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 1e3128bd0..7275da57a 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -131,7 +131,7 @@ class RequestHandler { // https://github.com/discordapp/discord-api-docs/issues/182 if (item.request.route.includes('reactions')) { - this.reset = Date.now() + getAPIOffset(serverDate) + 250; + this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250; } } From 989c365ef1c984462b3e7d9e6ba854c4629e36b8 Mon Sep 17 00:00:00 2001 From: Kyra Date: Wed, 29 Aug 2018 01:28:21 +0200 Subject: [PATCH 0863/1359] fix: Better global ratelimit management (#2801) * fix: better global ratelimit handling in RequestHandler fix: Remove useless line fix: Better global ratelimit management * refactor: Changed RESTManager#globallyRateLimited to be a getter * refactor: Remove RESTManager#globallyRateLimited getter * docs: Updated comments to reflect latest changes --- src/rest/RESTManager.js | 1 - src/rest/RequestHandler.js | 30 +++++++++++++++--------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 4b725f4d2..3d42b0b0e 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -9,7 +9,6 @@ class RESTManager { constructor(client, tokenPrefix = 'Bot') { this.client = client; this.handlers = new Collection(); - this.globallyRateLimited = false; this.tokenPrefix = tokenPrefix; this.versioned = true; this.globalTimeout = null; diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 7275da57a..67d645038 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -43,7 +43,7 @@ class RequestHandler { } get limited() { - return (this.manager.globallyRateLimited || this.remaining <= 0) && Date.now() < this.reset; + return (this.manager.globalTimeout || this.remaining <= 0) && Date.now() < this.reset; } get _inactive() { @@ -85,18 +85,8 @@ class RequestHandler { }); } - if (this.manager.globallyRateLimited && !this.manager.globalTimeout) { - // Set a global rate limit for all of the handlers instead of each one individually - this.manager.globalTimeout = this.manager.client.setTimeout(() => { - this.manager.globalTimeout = null; - this.manager.globallyRateLimited = false; - this.busy = false; - this.run(); - }, timeout); - } else if (this.manager.globalTimeout) { - // Already waiting for a global rate limit to clear - this.queue.unshift(item); - return null; + if (this.manager.globalTimeout) { + await this.manager.globalTimeout; } else { // Wait for the timeout to expire in order to avoid an actual 429 await Util.delayFor(timeout); @@ -116,8 +106,6 @@ class RequestHandler { } if (res && res.headers) { - if (res.headers.get('x-ratelimit-global')) this.manager.globallyRateLimited = true; - const serverDate = res.headers.get('date'); const limit = res.headers.get('x-ratelimit-limit'); const remaining = res.headers.get('x-ratelimit-remaining'); @@ -133,6 +121,18 @@ class RequestHandler { if (item.request.route.includes('reactions')) { this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250; } + + // Handle global ratelimit + if (res.headers.get('x-ratelimit-global')) { + // Set the manager's global timeout as the promise for other requests to "wait" + this.manager.globalTimeout = Util.delayFor(this.retryAfter); + + // Wait for the global timeout to resolve before continue + await this.manager.globalTimeout; + + // Clean up global timeout + this.manager.globalTimeout = null; + } } // Finished handling headers, safe to unlock manager From 8821fd40bc8156626db929431608bf7b9208a80c Mon Sep 17 00:00:00 2001 From: Adam Gauthier Date: Wed, 29 Aug 2018 02:43:28 -0400 Subject: [PATCH 0864/1359] cleanup: remove remaining apiRequestMethod docs and typings (#2806) --- src/util/Constants.js | 3 --- typings/index.d.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 7f5e7429b..b2e02c3ab 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -5,9 +5,6 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * all requests in the order they are triggered, whereas the burst handler runs multiple in parallel, and doesn't - * provide the guarantee of any particular order. Burst mode is more likely to hit a 429 ratelimit error by its nature, - * and is therefore slightly riskier to use. * @property {number} [shardId=0] ID of the shard to run * @property {number} [shardCount=0] Total number of shards * @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel diff --git a/typings/index.d.ts b/typings/index.d.ts index 244555d9e..d2dd4780e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1549,7 +1549,6 @@ declare module 'discord.js' { }; type ClientOptions = { - apiRequestMethod?: 'sequential' | 'burst'; presence?: PresenceData; shardId?: number; shardCount?: number; From c63f15c8bc98145c3e04955229648d69dbdf1678 Mon Sep 17 00:00:00 2001 From: Kyra Date: Wed, 29 Aug 2018 12:41:53 +0200 Subject: [PATCH 0865/1359] cleanup(Invite): remove outdated props, properly default to null (#2807) --- src/structures/Invite.js | 39 +++++++++++++-------------------------- typings/index.d.ts | 2 -- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 87e590a57..b142d6960 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -3,7 +3,7 @@ const Base = require('./Base'); /** * Represents an invitation to a guild channel. - * The only guaranteed properties are `code` and `channel`. Other properties can be missing. + * The only guaranteed properties are `code`, `channel`, and `url`. Other properties can be missing. * @extends {Base} */ class Invite extends Base { @@ -29,49 +29,37 @@ class Invite extends Base { * The approximate number of online members of the guild this invite is for * @type {?number} */ - this.presenceCount = data.approximate_presence_count || null; + this.presenceCount = 'approximate_presence_count' in data ? data.approximate_presence_count : null; /** * The approximate total number of members of the guild this invite is for * @type {?number} */ - this.memberCount = data.approximate_member_count || null; - - /** - * The number of text channels the guild this invite goes to has - * @type {?number} - */ - this.textChannelCount = data.guild ? data.guild.text_channel_count : null; - - /** - * The number of voice channels the guild this invite goes to has - * @type {?number} - */ - this.voiceChannelCount = data.guild ? data.guild.voice_channel_count : null; + this.memberCount = 'approximate_member_count' in data ? data.approximate_member_count : null; /** * Whether or not this invite is temporary * @type {?boolean} */ - this.temporary = data.temporary || null; + this.temporary = 'temporary' in data ? data.temporary : null; /** - * The maximum age of the invite, in seconds + * The maximum age of the invite, in seconds, 0 if never expires * @type {?number} */ - this.maxAge = data.max_age || null; + this.maxAge = 'max_age' in data ? data.max_age : null; /** * How many times this invite has been used * @type {?number} */ - this.uses = data.uses || null; + this.uses = 'uses' in data ? data.uses : null; /** * The maximum uses of this invite * @type {?number} */ - this.maxUses = data.max_uses || null; + this.maxUses = 'max_uses' in data ? data.max_uses : null; /** * The user who created this invite @@ -81,7 +69,7 @@ class Invite extends Base { /** * The channel the invite is for - * @type {GuildChannel} + * @type {Channel} */ this.channel = this.client.channels.add(data.channel, this.guild, false); @@ -89,7 +77,7 @@ class Invite extends Base { * The timestamp the invite was created at * @type {?number} */ - this.createdTimestamp = new Date(data.created_at).getTime() || null; + this.createdTimestamp = 'created_at' in data ? new Date(data.created_at).getTime() : null; } /** @@ -107,7 +95,7 @@ class Invite extends Base { * @readonly */ get expiresTimestamp() { - return this.createdTimestamp ? this.createdTimestamp + (this.maxAge * 1000) : null; + return this.createdTimestamp && this.maxAge ? this.createdTimestamp + (this.maxAge * 1000) : null; } /** @@ -116,7 +104,8 @@ class Invite extends Base { * @readonly */ get expiresAt() { - return this.expiresTimestamp ? new Date(this.expiresTimestamp) : null; + const { expiresTimestamp } = this; + return expiresTimestamp ? new Date(expiresTimestamp) : null; } /** @@ -154,8 +143,6 @@ class Invite extends Base { expiresTimestamp: true, presenceCount: false, memberCount: false, - textChannelCount: false, - voiceChannelCount: false, uses: false, channel: 'channelID', inviter: 'inviterID', diff --git a/typings/index.d.ts b/typings/index.d.ts index d2dd4780e..322691241 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -616,10 +616,8 @@ declare module 'discord.js' { public memberCount: number; public presenceCount: number; public temporary: boolean; - public textChannelCount: number; public readonly url: string; public uses: number; - public voiceChannelCount: number; public delete(reason?: string): Promise; public toJSON(): object; public toString(): string; From f75b80d96b1f2594fd68d6eaf560d423078668c3 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Thu, 30 Aug 2018 16:31:22 -0600 Subject: [PATCH 0866/1359] docs: add docstring to HTTPError --- src/rest/HTTPError.js | 4 ++++ src/rest/RequestHandler.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rest/HTTPError.js b/src/rest/HTTPError.js index 293be7c5c..32b3e6bd8 100644 --- a/src/rest/HTTPError.js +++ b/src/rest/HTTPError.js @@ -1,3 +1,7 @@ +/** + * Represents a HTTP error from a request. + * @extends Error + */ class HTTPError extends Error { constructor(message, name, code, method, path) { super(message); diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 67d645038..c6ea73ba5 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -127,7 +127,7 @@ class RequestHandler { // Set the manager's global timeout as the promise for other requests to "wait" this.manager.globalTimeout = Util.delayFor(this.retryAfter); - // Wait for the global timeout to resolve before continue + // Wait for the global timeout to resolve before continuing await this.manager.globalTimeout; // Clean up global timeout @@ -140,7 +140,7 @@ class RequestHandler { if (res.ok) { const success = await parseResponse(res); - // Nothing wrong with the request, proceed with the next + // Nothing wrong with the request, proceed with the next one resolve(success); return this.run(); } else if (res.status === 429) { From 3970c5005babd5ef360a87ce3c3e8fbdc3f66634 Mon Sep 17 00:00:00 2001 From: Ash <28841569+Asshley@users.noreply.github.com> Date: Fri, 31 Aug 2018 22:51:11 -0400 Subject: [PATCH 0867/1359] cleanup: use null over undefined (#2810) --- src/structures/MessageAttachment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index a8d74ee67..90ae4bbc2 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -66,13 +66,13 @@ class MessageAttachment { * The height of this attachment (if an image or video) * @type {?number} */ - this.height = data.height; + this.height = typeof data.height !== 'undefined' ? data.height : null; /** * The width of this attachment (if an image or video) * @type {?number} */ - this.width = data.width; + this.width = typeof data.width !== 'undefined' ? data.width : null; } toJSON() { From be4d6f9dc31229b3557b464a0937967d56d760f1 Mon Sep 17 00:00:00 2001 From: Isabella Date: Fri, 31 Aug 2018 21:51:35 -0500 Subject: [PATCH 0868/1359] feat: add ClientOptions#retryLimit (#2805) * feat: add ClientOptions#retryLimit * hydra needs to learn how to code right * a default would probably help * move incrementor & update comment * clarify docs on Infinity --- src/client/Client.js | 3 +++ src/rest/RESTManager.js | 1 + src/rest/RequestHandler.js | 6 +++--- src/util/Constants.js | 2 ++ typings/index.d.ts | 1 + 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index f3a6617d5..e285b9237 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -436,6 +436,9 @@ class Client extends BaseClient { if (!(options.disabledEvents instanceof Array)) { throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array'); } + if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number'); + } } } diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 3d42b0b0e..53a8047f5 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -41,6 +41,7 @@ class RESTManager { request: apiRequest, resolve, reject, + retries: 0, }).catch(reject); }); } diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index c6ea73ba5..99595b4b1 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -150,13 +150,13 @@ class RequestHandler { await Util.delayFor(this.retryAfter); return this.run(); } else if (res.status >= 500 && res.status < 600) { - // Retry once for possible serverside issues - if (item.retried) { + // Retry the specified number of times for possible serverside issues + if (item.retries === this.manager.client.options.retryLimit) { return reject( new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.route) ); } else { - item.retried = true; + item.retries++; this.queue.unshift(item); return this.run(); } diff --git a/src/util/Constants.js b/src/util/Constants.js index b2e02c3ab..c4ab29a93 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -23,6 +23,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * requests (higher values will reduce rate-limiting errors on bad connections) * @property {number} [restSweepInterval=60] How frequently to delete inactive request buckets, in seconds * (or 0 for never) + * @property {number} [retryLimit=1] How many times to retry on 5XX errors (Infinity for indefinite amount of retries) * @property {PresenceData} [presence] Presence data to use upon login * @property {WSEventType[]} [disabledEvents] An array of disabled websocket events. Events in this array will not be * processed, potentially resulting in performance improvements for larger bots. Only disable events you are @@ -42,6 +43,7 @@ exports.DefaultOptions = { disableEveryone: false, restWsBridgeTimeout: 5000, disabledEvents: [], + retryLimit: 1, restTimeOffset: 500, restSweepInterval: 60, presence: {}, diff --git a/typings/index.d.ts b/typings/index.d.ts index 322691241..12f38f31e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1557,6 +1557,7 @@ declare module 'discord.js' { disableEveryone?: boolean; restWsBridgeTimeout?: number; restTimeOffset?: number; + retryLimit?: number, disabledEvents?: WSEventType[]; ws?: WebSocketOptions; http?: HTTPOptions; From 314161ab70ab2c7f59a0ebd02a391f8d86186cdc Mon Sep 17 00:00:00 2001 From: lipgloss Date: Fri, 31 Aug 2018 20:52:50 -0600 Subject: [PATCH 0869/1359] GuildEmoji.fetchAuthor() error handling (#2788) * handle when client has insufficient permissions, add typing * code review --- src/errors/Messages.js | 2 ++ src/structures/GuildEmoji.js | 2 ++ typings/index.d.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 00f99a676..80fa7e389 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -95,6 +95,8 @@ const Messages = { EMOJI_TYPE: 'Emoji must be a string or GuildEmoji/ReactionEmoji', EMOJI_MANAGED: 'Emoji is managed and has no Author.', + MISSING_MANAGE_EMOJIS_PERMISSION: + guild => `Client must have Manage Emoji permission in guild ${guild} to see emoji authors.`, REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.', diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index e9fde762a..20e48aa06 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -90,6 +90,8 @@ class GuildEmoji extends Emoji { fetchAuthor() { if (this.managed) { return Promise.reject(new Error('EMOJI_MANAGED')); + } else if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { + return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild)); } return this.client.api.guilds(this.guild.id).emojis(this.id).get() .then(emoji => this.client.users.add(emoji.user)); diff --git a/typings/index.d.ts b/typings/index.d.ts index 12f38f31e..808fcf6e4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -545,6 +545,7 @@ declare module 'discord.js' { public delete(reason?: string): Promise; public edit(data: GuildEmojiEditData, reason?: string): Promise; public equals(other: GuildEmoji | object): boolean; + public fetchAuthor(): Promise; public setName(name: string, reason?: string): Promise; } From 038b142db287353f205bc954d3653e175d52c436 Mon Sep 17 00:00:00 2001 From: Adam Gauthier Date: Fri, 31 Aug 2018 23:02:30 -0400 Subject: [PATCH 0870/1359] fix: emit ReactionCollector#remove when reaction is removed by collected user (#2803) --- src/structures/ReactionCollector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index b0a2130eb..5c4532e2f 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -105,7 +105,8 @@ class ReactionCollector extends Collector { * @param {MessageReaction} reaction The reaction that was removed * @param {User} user The user that removed the reaction */ - if (this.collected.has(ReactionCollector.key(reaction))) { + if (this.collected.has(ReactionCollector.key(reaction)) && + this.users.has(user.id)) { this.emit('remove', reaction, user); } return reaction.count ? null : ReactionCollector.key(reaction); From b1ce602e79be6f56978a6ca3b0900838697d99f7 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 2 Sep 2018 10:42:22 +0200 Subject: [PATCH 0871/1359] fix/cleanup(typings): move static properties above methods Make BitField.FLAGS declaration less strict, allowing enums and typed dicts --- typings/index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 808fcf6e4..32dac849d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -36,8 +36,8 @@ declare module 'discord.js' { } export class ActivityFlags extends BitField { - public static resolve(permission: BitFieldResolvable): number; public static FLAGS: Record; + public static resolve(permission: BitFieldResolvable): number; } export class APIMessage { @@ -108,8 +108,8 @@ declare module 'discord.js' { public toJSON(): number; public valueOf(): number; public [Symbol.iterator](): Iterator; + public static FLAGS: object; public static resolve(bit?: BitFieldResolvable): number; - public static FLAGS: { [key: string]: number }; } export class CategoryChannel extends GuildChannel { @@ -1014,8 +1014,8 @@ declare module 'discord.js' { } export class Speaking extends BitField { - public static resolve(permission: BitFieldResolvable): number; public static FLAGS: Record; + public static resolve(permission: BitFieldResolvable): number; } export class Structures { From 6b9dd13d43a8e8fd567151d5d4e6769925189f65 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 2 Sep 2018 13:23:39 +0200 Subject: [PATCH 0872/1359] docs/typings(GuildChannel): permissionsfor returns a nullable readonly Permissions instance --- src/structures/GuildChannel.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index ffae8c36b..98dfd4d92 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -94,7 +94,7 @@ class GuildChannel extends Channel { /** * Gets the overall set of permissions for a member or role in this channel, taking into account channel overwrites. * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for - * @returns {?Permissions} + * @returns {?Readonly} */ permissionsFor(memberOrRole) { const member = this.guild.members.resolve(memberOrRole); diff --git a/typings/index.d.ts b/typings/index.d.ts index 32dac849d..c36e6a1f7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,7 +523,7 @@ declare module 'discord.js' { options: Array> | Collection>, reason?: string ): Promise; - public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Permissions; + public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly; public setName(name: string, reason?: string): Promise; public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; From ead99a53b7c1838fa2061d22f7ee07a3612bfb3f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 2 Sep 2018 14:57:36 +0200 Subject: [PATCH 0873/1359] fix(HTTPError): the path property should actually be the path --- src/rest/RequestHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 99595b4b1..3f66fd438 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -153,7 +153,7 @@ class RequestHandler { // Retry the specified number of times for possible serverside issues if (item.retries === this.manager.client.options.retryLimit) { return reject( - new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.route) + new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path) ); } else { item.retries++; @@ -170,7 +170,7 @@ class RequestHandler { return null; } catch (err) { return reject( - new HTTPError(err.message, err.constructor.name, err.status, request.method, request.route) + new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path) ); } } From 75745fe2ba06b769c1e6754dca3227fad601ed96 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 2 Sep 2018 16:03:03 +0200 Subject: [PATCH 0874/1359] typings(GuildChannel): permissionsFor's return value is nullable --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c36e6a1f7..2c3a4d98c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,7 +523,7 @@ declare module 'discord.js' { options: Array> | Collection>, reason?: string ): Promise; - public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly; + public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; public setName(name: string, reason?: string): Promise; public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; From b068abb5deb5e60a361278efdb8ae1b9dbaf39b0 Mon Sep 17 00:00:00 2001 From: lipgloss Date: Mon, 3 Sep 2018 00:53:20 -0600 Subject: [PATCH 0875/1359] typings: add declaration of Message#url and MessageAttachment#size (#2821) * add url to message typings * also its readonly * message attachment sizing typing --- typings/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 2c3a4d98c..7eca0f3a6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -657,6 +657,7 @@ declare module 'discord.js' { public system: boolean; public tts: boolean; public type: MessageType; + public readonly url: string; public webhookID: Snowflake; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; @@ -682,6 +683,7 @@ declare module 'discord.js' { public id: Snowflake; public name?: string; public proxyURL: string; + public size: number; public url: string; public width: number; public setFile(attachment: BufferResolvable | Stream, name?: string): this; From e96a60361ad76a9aee9205d09b2962e0d7979db1 Mon Sep 17 00:00:00 2001 From: Ash <28841569+Asshley@users.noreply.github.com> Date: Mon, 3 Sep 2018 03:11:52 -0400 Subject: [PATCH 0876/1359] feat(TextBasedChannel): add lastPinTimestamp and lastPinAt (#2813) * add lastPinTimestamp * typings * use or instead of ternary --- .../packets/handlers/ChannelPinsUpdate.js | 7 ++++++- src/structures/DMChannel.js | 7 +++++++ src/structures/GroupDMChannel.js | 7 +++++++ src/structures/TextChannel.js | 7 +++++++ src/structures/interfaces/TextBasedChannel.js | 16 ++++++++++++++++ typings/index.d.ts | 2 ++ 6 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js index 6332ec463..ea6c8f31d 100644 --- a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js +++ b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js @@ -16,7 +16,12 @@ class ChannelPinsUpdate extends AbstractHandler { const data = packet.d; const channel = client.channels.get(data.channel_id); const time = new Date(data.last_pin_timestamp); - if (channel && time) client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); + if (channel && time) { + // Discord sends null for last_pin_timestamp if the last pinned message was removed + channel.lastPinTimestamp = time.getTime() || null; + + client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); + } } } diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 5c784b3a3..1f45f490f 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -32,6 +32,12 @@ class DMChannel extends Channel { * @type {?Snowflake} */ this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; } /** @@ -49,6 +55,7 @@ class DMChannel extends Channel { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ get lastMessage() {} + get lastPinAt() {} send() {} search() {} startTyping() {} diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 933c701bb..b2fe7200f 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -103,6 +103,12 @@ class GroupDMChannel extends Channel { * @type {?Snowflake} */ this.lastMessageID = data.last_message_id; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; } /** @@ -219,6 +225,7 @@ class GroupDMChannel extends Channel { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ get lastMessage() {} + get lastPinAt() {} send() {} search() {} startTyping() {} diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index df734a748..698e23f20 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -43,6 +43,12 @@ class TextChannel extends GuildChannel { */ this.lastMessageID = data.last_message_id; + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; + if (data.messages) for (const message of data.messages) this.messages.add(message); } @@ -101,6 +107,7 @@ class TextChannel extends GuildChannel { // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ get lastMessage() {} + get lastPinAt() {} send() {} search() {} startTyping() {} diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 207f74248..45eecf750 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -21,6 +21,12 @@ class TextBasedChannel { * @type {?Snowflake} */ this.lastMessageID = null; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = null; } /** @@ -32,6 +38,15 @@ class TextBasedChannel { return this.messages.get(this.lastMessageID) || null; } + /** + * The date when the last pinned message was pinned, if there was one + * @type {?Date} + * @readonly + */ + get lastPinAt() { + return this.lastPinTimestamp ? new Date(this.lastPinTimestamp) : null; + } + /** * Options provided when sending or editing a message. * @typedef {Object} MessageOptions @@ -322,6 +337,7 @@ class TextBasedChannel { if (full) { props.push( 'lastMessage', + 'lastPinAt', 'bulkDelete', 'startTyping', 'stopTyping', diff --git a/typings/index.d.ts b/typings/index.d.ts index 7eca0f3a6..555272ad6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1390,6 +1390,8 @@ declare module 'discord.js' { lastMessageID: Snowflake; lastMessageChannelID: Snowflake; readonly lastMessage: Message; + lastPinTimestamp: number; + readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; send(options?: MessageOptions | MessageAdditions): Promise; }; From 9c2924a1b49d2621abd97f3354fe2a64eb134141 Mon Sep 17 00:00:00 2001 From: Ash <28841569+Asshley@users.noreply.github.com> Date: Wed, 5 Sep 2018 08:33:44 -0400 Subject: [PATCH 0877/1359] docs(ChannelPinsUpdate): clarify time param (#2823) --- src/client/websocket/packets/handlers/ChannelPinsUpdate.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js index ea6c8f31d..702be2253 100644 --- a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js +++ b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js @@ -30,7 +30,8 @@ module.exports = ChannelPinsUpdate; /** * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, not much information * can be provided easily here - you need to manually check the pins yourself. + * The `time` parameter will be a Unix Epoch Date object when there are no pins left. * @event Client#channelPinsUpdate * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in - * @param {Date} time The time of the pins update + * @param {Date} time The time when the last pinned message was pinned */ From 1d77d1a9b89302316cf0093cffbb5e274c5a601e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 10 Sep 2018 13:41:33 +0200 Subject: [PATCH 0878/1359] fix(typings): remove UserConnection, fix various constructors Closes #2825 --- typings/index.d.ts | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 555272ad6..21655e0fb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -16,7 +16,7 @@ declare module 'discord.js' { //#region Classes export class Activity { - constructor(presence: Presence, data: object); + constructor(presence: Presence, data?: object); public applicationID: Snowflake; public assets: RichPresenceAssets; public details: string; @@ -117,7 +117,7 @@ declare module 'discord.js' { } export class Channel extends Base { - constructor(client: Client, data: object); + constructor(client: Client, data?: object); public readonly createdAt: Date; public readonly createdTimestamp: number; public deleted: boolean; @@ -342,7 +342,7 @@ declare module 'discord.js' { } export class DiscordAPIError extends Error { - constructor(path: string, error: object); + constructor(path: string, error: object, method: string); private static flattenErrors(obj: object, key: string): string[]; public code: number; @@ -351,7 +351,7 @@ declare module 'discord.js' { } export class DMChannel extends TextBasedChannel(Channel) { - constructor(client: Client, data: object); + constructor(client: Client, data?: object); public messages: MessageStore; public recipient: User; } @@ -369,7 +369,7 @@ declare module 'discord.js' { } export class GroupDMChannel extends TextBasedChannel(Channel) { - constructor(client: Client, data: object); + constructor(client: Client, data?: object); public applicationID: Snowflake; public icon: string; public managed: boolean; @@ -482,7 +482,7 @@ declare module 'discord.js' { } class GuildAuditLogsEntry { - constructor(guild: Guild, data: object); + constructor(logs: GuildAuditLogs, guild: Guild, data: object); public action: GuildAuditLogsAction; public actionType: GuildAuditLogsActionType; public changes: AuditLogChange[]; @@ -498,7 +498,7 @@ declare module 'discord.js' { } export class GuildChannel extends Channel { - constructor(guild: Guild, data: object); + constructor(guild: Guild, data?: object); private memberPermissions(member: GuildMember): Readonly; private rolePermissions(role: Role): Readonly; @@ -676,7 +676,7 @@ declare module 'discord.js' { } export class MessageAttachment { - constructor(attachment: BufferResolvable | Stream, name?: string); + constructor(attachment: BufferResolvable | Stream, name?: string, data?: object); public attachment: BufferResolvable | Stream; public height: number; @@ -738,7 +738,7 @@ declare module 'discord.js' { } export class MessageMentions { - constructor(message: Message, users: any[], roles: any[], everyone: boolean); + constructor(message: Message, users: object[] | Collection, roles: Snowflake[] | Collection, everyone: boolean); private _channels: Collection; private readonly _content: Message; private _members: Collection; @@ -776,7 +776,7 @@ declare module 'discord.js' { } export class PermissionOverwrites { - constructor(guildChannel: GuildChannel, data: object); + constructor(guildChannel: GuildChannel, data?: object); public allow: Readonly; public readonly channel: GuildChannel; public deny: Readonly; @@ -796,7 +796,7 @@ declare module 'discord.js' { } export class Presence { - constructor(client: Client, data: object); + constructor(client: Client, data?: object); public activity: Activity; public flags: Readonly; public status: 'online' | 'offline' | 'idle' | 'dnd'; @@ -884,7 +884,7 @@ declare module 'discord.js' { } export class Shard extends EventEmitter { - constructor(manager: ShardingManager, id: number, args?: string[]); + constructor(manager: ShardingManager, id: number); private _evals: Map>; private _exitListener: Function; private _fetches: Map>; @@ -1028,7 +1028,7 @@ declare module 'discord.js' { } export class TextChannel extends TextBasedChannel(GuildChannel) { - constructor(guild: Guild, data: object); + constructor(guild: Guild, data?: object); public readonly members: Collection; public messages: MessageStore; public nsfw: boolean; @@ -1063,17 +1063,6 @@ declare module 'discord.js' { public typingSinceIn(channel: ChannelResolvable): Date; } - export class UserConnection { - constructor(user: User, data: object); - public id: string; - public integrations: object[]; - public name: string; - public revoked: boolean; - public type: string; - public user: User; - public toJSON(): object; - } - export class Util { public static basename(path: string, ext?: string): string; public static binaryToID(num: string): Snowflake; @@ -1127,7 +1116,7 @@ declare module 'discord.js' { } export class VoiceChannel extends GuildChannel { - constructor(guild: Guild, data: object); + constructor(guild: Guild, data?: object); public bitrate: number; public readonly connection: VoiceConnection; public readonly full: boolean; @@ -1258,7 +1247,7 @@ declare module 'discord.js' { } export class Webhook extends WebhookMixin() { - constructor(client: Client, data: object); + constructor(client: Client, data?: object); public avatar: string; public channelID: Snowflake; public guildID: Snowflake; From 2cc0a31d5cb3356f99241e998b6e91d3bc3785e3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 15 Sep 2018 19:49:34 +0200 Subject: [PATCH 0879/1359] typings(Message): member property is a readonly getter --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 21655e0fb..a896dd5a0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -648,7 +648,7 @@ declare module 'discord.js' { public readonly guild: Guild; public hit: boolean; public id: Snowflake; - public member: GuildMember; + public readonly member: GuildMember; public mentions: MessageMentions; public nonce: string; public readonly pinnable: boolean; From d2da771e0f28773587766ede94cfdae222deefa0 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 18 Sep 2018 11:04:54 +0200 Subject: [PATCH 0880/1359] docs(Client): fix syntax error in fetchInvite example --- src/client/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index e285b9237..d55b2d88e 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -257,7 +257,7 @@ class Client extends BaseClient { * @returns {Promise} * @example * client.fetchInvite('https://discord.gg/bRCvFy9') - * .then(invite => console.log(`Obtained invite with code: ${invite.code}`) + * .then(invite => console.log(`Obtained invite with code: ${invite.code}`)) * .catch(console.error); */ fetchInvite(invite) { From 8a6d029c006b5a107363d3787ff60bcccda2a984 Mon Sep 17 00:00:00 2001 From: Crawl Date: Wed, 19 Sep 2018 01:20:07 +0200 Subject: [PATCH 0881/1359] build(dev-deps): update to use terser instead of uglifyjs --- package.json | 4 ++-- webpack.config.js | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9f33875bd..4f27bf482 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,11 @@ "ws": "^6.0.0" }, "peerDependencies": { + "@discordjs/uws": "^10.149.0", "bufferutil": "^4.0.0", "erlpack": "discordapp/erlpack", "libsodium-wrappers": "^0.7.3", "sodium": "^2.0.3", - "@discordjs/uws": "^10.149.0", "zlib-sync": "^0.1.4" }, "devDependencies": { @@ -55,10 +55,10 @@ "discord.js-docgen": "discordjs/docgen", "eslint": "^5.4.0", "json-filter-loader": "^1.0.0", + "terser-webpack-plugin": "^1.1.0", "tslint": "^5.11.0", "tslint-config-typings": "^0.3.1", "typescript": "^3.0.1", - "uglifyjs-webpack-plugin": "^1.3.0", "webpack": "^4.17.0", "webpack-cli": "^3.1.0" }, diff --git a/webpack.config.js b/webpack.config.js index b845d3bd0..c7416852d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,6 @@ const path = require('path'); const webpack = require('webpack'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); +const TerserJSPlugin = require('terser-webpack-plugin'); const version = require('./package.json').version; const prod = process.env.NODE_ENV === 'production'; @@ -46,12 +46,13 @@ module.exports = { }, optimization: { minimizer: [ - new UglifyJSPlugin({ - uglifyOptions: { + new TerserJSPlugin({ + terserOptions: { mangle: { keep_classnames: true }, compress: { keep_classnames: true }, output: { comments: false }, }, + parallel: true, }), ], }, From 6d184257b36bc66063d50ae38dbd4e13381e8ac2 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 21 Sep 2018 03:59:58 -0500 Subject: [PATCH 0882/1359] refactor: remove duplicate send checks (#2790) * re-direct pr code to master * fix Webhook send docs * requested changes * typings: Updated to latest commit * requested change * requested change --- src/structures/APIMessage.js | 59 +++++++++++++++++-- src/structures/Message.js | 13 ++-- src/structures/Webhook.js | 27 +++------ src/structures/interfaces/TextBasedChannel.js | 27 ++++----- typings/index.d.ts | 16 +++-- 5 files changed, 92 insertions(+), 50 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 77eb4234b..bb4d16179 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -25,6 +25,18 @@ class APIMessage { * @type {MessageOptions|WebhookMessageOptions} */ this.options = options; + + /** + * Data sendable to the API + * @type {?Object} + */ + this.data = null; + + /** + * Files sendable to the API + * @type {?Object[]} + */ + this.files = null; } /** @@ -100,9 +112,11 @@ class APIMessage { /** * Resolves data. - * @returns {Object} + * @returns {APIMessage} */ resolveData() { + if (this.data) return this; + const content = this.makeContent(); const tts = Boolean(this.options.tts); let nonce; @@ -128,7 +142,7 @@ class APIMessage { if (this.options.avatarURL) avatarURL = this.options.avatarURL; } - return { + this.data = { content, tts, nonce, @@ -137,13 +151,16 @@ class APIMessage { username, avatar_url: avatarURL, }; + return this; } /** * Resolves files. - * @returns {Promise} + * @returns {Promise} */ - resolveFiles() { + async resolveFiles() { + if (this.files) return this; + const embedLikes = []; if (this.isWebhook) { if (this.options.embeds) { @@ -163,7 +180,39 @@ class APIMessage { } } - return Promise.all(fileLikes.map(f => this.constructor.resolveFile(f))); + this.files = await Promise.all(fileLikes.map(f => this.constructor.resolveFile(f))); + return this; + } + + /** + * Converts this APIMessage into an array of APIMessages for each split content + * @returns {APIMessage[]} + */ + split() { + if (!this.data) this.resolveData(); + + if (!(this.data.content instanceof Array)) return [this]; + + const apiMessages = []; + + for (let i = 0; i < this.data.content.length; i++) { + let data; + let opt; + + if (i === this.data.content.length - 1) { + data = { ...this.data, content: this.data.content[i] }; + opt = { ...this.options, content: this.data.content[i] }; + } else { + data = { content: this.data.content[i], tts: this.data.tts }; + opt = { content: this.data.content[i], tts: this.data.tts }; + } + + const apiMessage = new APIMessage(this.target, opt); + apiMessage.data = data; + apiMessages.push(apiMessage); + } + + return apiMessages; } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index e229e20cf..88a1707a9 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -359,7 +359,7 @@ class Message extends Base { /** * Edits the content of the message. - * @param {StringResolvable} [content=''] The new content for the message + * @param {StringResolvable|APIMessage} [content=''] The new content for the message * @param {MessageEditOptions|MessageEmbed} [options] The options to provide * @returns {Promise} * @example @@ -369,7 +369,9 @@ class Message extends Base { * .catch(console.error); */ edit(content, options) { - const data = APIMessage.create(this, content, options).resolveData(); + const { data } = content instanceof APIMessage ? + content.resolveData() : + APIMessage.create(this, content, options).resolveData(); return this.client.api.channels[this.channel.id].messages[this.id] .patch({ data }) .then(d => { @@ -458,7 +460,7 @@ class Message extends Base { /** * Replies to the message. - * @param {StringResolvable} [content=''] The content for the message + * @param {StringResolvable|APIMessage} [content=''] The content for the message * @param {MessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example @@ -468,7 +470,10 @@ class Message extends Base { * .catch(console.error); */ reply(content, options) { - return this.channel.send(APIMessage.transformOptions(content, options, { reply: this.member || this.author })); + return this.channel.send(content instanceof APIMessage ? + content : + APIMessage.transformOptions(content, options, { reply: this.member || this.author }) + ); } /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 711b3db79..a719249ea 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -84,7 +84,7 @@ class Webhook { /** * Sends a message with this webhook. - * @param {StringResolvable} [content=''] The content to send + * @param {StringResolvable|APIMessage} [content=''] The content to send * @param {WebhookMessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example @@ -126,27 +126,18 @@ class Webhook { * .catch(console.error); */ async send(content, options) { - const apiMessage = APIMessage.create(this, content, options); - const data = apiMessage.resolveData(); - if (data.content instanceof Array) { - const messages = []; - for (let i = 0; i < data.content.length; i++) { - let opt; - if (i === data.content.length - 1) { - opt = { embeds: data.embeds, files: apiMessage.options.files }; - } else { - opt = {}; - } + let apiMessage; - 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); + if (content instanceof apiMessage) { + apiMessage = content.resolveData(); + } else { + apiMessage = APIMessage.create(this, content, options).resolveData(); + if (apiMessage.data.content instanceof Array) { + return Promise.all(apiMessage.split().map(this.send.bind(this))); } - return messages; } - const files = await apiMessage.resolveFiles(); + const { data, files } = await apiMessage.resolveFiles(); return this.client.api.webhooks(this.id, this.token).post({ data, files, query: { wait: true }, diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 45eecf750..7f9553f1e 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -81,7 +81,7 @@ class TextBasedChannel { /** * Sends a message to this channel. - * @param {StringResolvable} [content=''] The content to send + * @param {StringResolvable|APIMessage} [content=''] The content to send * @param {MessageOptions|MessageAdditions} [options={}] The options to provide * @returns {Promise} * @example @@ -125,30 +125,23 @@ class TextBasedChannel { async send(content, options) { const User = require('../User'); const GuildMember = require('../GuildMember'); + if (this instanceof User || this instanceof GuildMember) { return this.createDM().then(dm => dm.send(content, options)); } - const apiMessage = APIMessage.create(this, content, options); - const data = apiMessage.resolveData(); - if (data.content instanceof Array) { - const messages = []; - for (let i = 0; i < data.content.length; i++) { - let opt; - if (i === data.content.length - 1) { - opt = { tts: data.tts, embed: data.embed, files: apiMessage.options.files }; - } else { - opt = { tts: data.tts }; - } + let apiMessage; - // eslint-disable-next-line no-await-in-loop - const message = await this.send(data.content[i], opt); - messages.push(message); + if (content instanceof APIMessage) { + apiMessage = content.resolveData(); + } else { + apiMessage = APIMessage.create(this, content, options).resolveData(); + if (apiMessage.data.content instanceof Array) { + return Promise.all(apiMessage.split().map(this.send.bind(this))); } - return messages; } - const files = await apiMessage.resolveFiles(); + const { data, files } = await apiMessage.resolveFiles(); return this.client.api.channels[this.id].messages.post({ data, files }) .then(d => this.client.actions.MessageCreate.handle(d).message); } diff --git a/typings/index.d.ts b/typings/index.d.ts index a896dd5a0..39ddc2974 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -42,8 +42,10 @@ declare module 'discord.js' { export class APIMessage { constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions); + public data?: object; public readonly isUser: boolean; public readonly isWebhook: boolean; + public files?: object[]; public options: MessageOptions | WebhookMessageOptions; public target: MessageTarget; @@ -63,8 +65,10 @@ declare module 'discord.js' { ): MessageOptions | WebhookMessageOptions; public makeContent(): string | string[]; - public resolveData(): object; - public resolveFiles(): Promise; + public resolve(): Promise; + public resolveData(): this; + public resolveFiles(): Promise; + public split(): APIMessage[]; } export class Base { @@ -663,13 +667,13 @@ declare module 'discord.js' { public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; public edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise; - public edit(options: MessageEditOptions | MessageEmbed): Promise; + public edit(options: MessageEditOptions | MessageEmbed | APIMessage): Promise; public equals(message: Message, rawData: object): boolean; public fetchWebhook(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - public reply(options?: MessageOptions | MessageAdditions): Promise; + public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; public toJSON(): object; public toString(): string; public unpin(): Promise; @@ -1382,7 +1386,7 @@ declare module 'discord.js' { lastPinTimestamp: number; readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - send(options?: MessageOptions | MessageAdditions): Promise; + send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; }; type TextBasedChannelFields = { @@ -1404,7 +1408,7 @@ declare module 'discord.js' { delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise; - send(options?: WebhookMessageOptions | MessageAdditions): Promise; + send(options?: WebhookMessageOptions | MessageAdditions | APIMessage): Promise; sendSlackMessage(body: object): Promise; }; From 3d8207a3db2e555930d4bf809fe1bc98ec9cb348 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 21 Sep 2018 05:21:51 -0500 Subject: [PATCH 0883/1359] refactor: comprehensive permissionOverwrites refactor (#2818) * wip: comprehensive permissionOverwrites refactor * PermissionOverwrites.resolve should Promise.reject() where a promise is the expected return value * On second thought, async rewrite to automatically reject on throw * Fix some docs * Fix a bug * fix 2 more bugs * typings: Updated for latest commit * typings: Add missing method in GuildChannel * typings: Add missing `| null` in PermissionOverwriteOption type * Suggested changes --- src/stores/GuildChannelStore.js | 14 ++- src/structures/GuildChannel.js | 118 ++++++++----------- src/structures/PermissionOverwrites.js | 119 ++++++++++++++++++++ src/structures/shared/resolvePermissions.js | 26 ----- src/util/Util.js | 4 +- typings/index.d.ts | 39 +++++-- 6 files changed, 202 insertions(+), 118 deletions(-) delete mode 100644 src/structures/shared/resolvePermissions.js diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index d775622d7..1532f5920 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -2,7 +2,7 @@ const Channel = require('../structures/Channel'); const { ChannelTypes } = require('../util/Constants'); const DataStore = require('./DataStore'); const GuildChannel = require('../structures/GuildChannel'); -const resolvePermissions = require('../structures/shared/resolvePermissions'); +const PermissionOverwrites = require('../structures/PermissionOverwrites'); /** * Stores guild channels. @@ -31,7 +31,7 @@ class GuildChannelStore extends DataStore { * @param {number} [options.bitrate] Bitrate of the new channel in bits (only voice) * @param {number} [options.userLimit] Maximum amount of users allowed in the new channel (only voice) * @param {ChannelResolvable} [options.parent] Parent of the new channel - * @param {OverwriteData[]|Collection} [options.overwrites] + * @param {OverwriteResolvable[]|Collection} [options.overwrites] * Permission overwrites of the new channel * @param {string} [options.reason] Reason for creating the channel * @returns {Promise} @@ -52,9 +52,10 @@ class GuildChannelStore extends DataStore { * ], * }) */ - create(name, { type, topic, nsfw, bitrate, userLimit, parent, overwrites, reason } = {}) { + async create(name, { type, topic, nsfw, bitrate, userLimit, parent, overwrites, reason } = {}) { if (parent) parent = this.client.channels.resolveID(parent); - return this.client.api.guilds(this.guild.id).channels.post({ + + const data = await this.client.api.guilds(this.guild.id).channels.post({ data: { name, topic, @@ -63,10 +64,11 @@ class GuildChannelStore extends DataStore { bitrate, user_limit: userLimit, parent_id: parent, - permission_overwrites: resolvePermissions.call(this, overwrites), + permission_overwrites: overwrites && overwrites.map(o => PermissionOverwrites.resolve(o, this.guild)), }, reason, - }).then(data => this.client.actions.ChannelCreate.handle(data).channel); + }); + return this.client.actions.ChannelCreate.handle(data).channel; } /** diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 98dfd4d92..0acf59676 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -1,7 +1,6 @@ const Channel = require('./Channel'); const Role = require('./Role'); const Invite = require('./Invite'); -const resolvePermissions = require('./shared/resolvePermissions'); const PermissionOverwrites = require('./PermissionOverwrites'); const Util = require('../util/Util'); const Permissions = require('../util/Permissions'); @@ -73,11 +72,11 @@ class GuildChannel extends Channel { get permissionsLocked() { if (!this.parent) return null; if (this.permissionOverwrites.size !== this.parent.permissionOverwrites.size) return false; - return !this.permissionOverwrites.find((value, key) => { + return this.permissionOverwrites.every((value, key) => { const testVal = this.parent.permissionOverwrites.get(key); - return testVal === undefined || - testVal.deny.bitfield !== value.deny.bitfield || - testVal.allow.bitfield !== value.allow.bitfield; + return testVal !== undefined && + testVal.deny.bitfield === value.deny.bitfield && + testVal.allow.bitfield === value.allow.bitfield; }); } @@ -179,7 +178,7 @@ class GuildChannel extends Channel { /** * Replaces the permission overwrites in this channel. * @param {Object} [options] Options - * @param {OverwriteData[]|Collection} [options.overwrites] + * @param {OverwriteResolvable[]|Collection} [options.overwrites] * Permission overwrites the channel gets updated with * @param {string} [options.reason] Reason for updating the channel overwrites * @returns {Promise} @@ -195,30 +194,18 @@ class GuildChannel extends Channel { * }); */ overwritePermissions({ overwrites, reason } = {}) { - return this.edit({ permissionOverwrites: resolvePermissions.call(this, overwrites), reason }) + return this.edit({ permissionOverwrites: overwrites, reason }) .then(() => this); } /** - * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). - * ```js - * { - * 'SEND_MESSAGES': true, - * 'EMBED_LINKS': null, - * 'ATTACH_FILES': false, - * } - * ``` - * @typedef {Object} PermissionOverwriteOption - */ - - /** - * Overwrites the permissions for a user or role in this channel. + * Updates Overwrites for a user or role in this channel. (creates if non-existent) * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update * @param {PermissionOverwriteOption} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} * @example - * // Overwrite permissions for a message author + * // Update or Create permission overwrites for a message author * message.channel.updateOverwrite(message.author, { * SEND_MESSAGES: false * }) @@ -226,40 +213,34 @@ class GuildChannel extends Channel { * .catch(console.error); */ updateOverwrite(userOrRole, options, reason) { - const allow = new Permissions(); - const deny = new Permissions(); - let type; + userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole); + if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true)); - const role = this.guild.roles.get(userOrRole); + const existing = this.permissionOverwrites.get(userOrRole.id); + if (existing) return existing.update(options, reason).then(() => this); + return this.createOverwrite(userOrRole, options, reason); + } - if (role || userOrRole instanceof Role) { - userOrRole = role || userOrRole; - type = 'role'; - } else { - userOrRole = this.client.users.resolve(userOrRole); - type = 'member'; - if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true)); - } + /** + * Overwrites the permissions for a user or role in this channel. (replaces if existent) + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOption} options The options for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise} + * @example + * // Create or Replace permissions overwrites for a message author + * message.channel.createOverwrite(message.author, { + * SEND_MESSAGES: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + createOverwrite(userOrRole, options, reason) { + userOrRole = this.guild.roles.resolve(userOrRole) || this.client.users.resolve(userOrRole); + if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true)); - const prevOverwrite = this.permissionOverwrites.get(userOrRole.id); - - if (prevOverwrite) { - allow.add(prevOverwrite.allow); - deny.add(prevOverwrite.deny); - } - - for (const perm in options) { - if (options[perm] === true) { - allow.add(Permissions.FLAGS[perm]); - deny.remove(Permissions.FLAGS[perm]); - } else if (options[perm] === false) { - allow.remove(Permissions.FLAGS[perm]); - deny.add(Permissions.FLAGS[perm]); - } else if (options[perm] === null) { - allow.remove(Permissions.FLAGS[perm]); - deny.remove(Permissions.FLAGS[perm]); - } - } + const type = userOrRole instanceof Role ? 'role' : 'member'; + const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options); return this.client.api.channels(this.id).permissions[userOrRole.id] .put({ data: { id: userOrRole.id, type, allow: allow.bitfield, deny: deny.bitfield }, reason }) @@ -272,12 +253,7 @@ class GuildChannel extends Channel { */ lockPermissions() { if (!this.parent) return Promise.reject(new Error('GUILD_CHANNEL_ORPHAN')); - const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => ({ - deny: overwrite.deny.bitfield, - allow: overwrite.allow.bitfield, - id: overwrite.id, - type: overwrite.type, - })); + const permissionOverwrites = this.parent.permissionOverwrites.map(overwrite => overwrite.toJSON()); return this.edit({ permissionOverwrites }); } @@ -308,18 +284,10 @@ class GuildChannel extends Channel { * @property {Snowflake} [parentID] The parent ID of the channel * @property {boolean} [lockPermissions] * Lock the permissions of the channel to what the parent's permissions are - * @property {OverwriteData[]|Collection} [permissionOverwrites] + * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] * Permission overwrites for the channel */ - /** - * The data for a permission overwrite - * @typedef {Object} OverwriteData - * @property {PermissionResolvable} [allow] The permissions to allow - * @property {PermissionResolvable} [deny] The permissions to deny - * @property {GuildMemberResolvable|RoleResolvable} memberOrRole Member or role this overwrite is for - */ - /** * Edits the channel. * @param {ChannelData} data The new data for the channel @@ -342,7 +310,11 @@ class GuildChannel extends Channel { }); }); } - return this.client.api.channels(this.id).patch({ + + const permission_overwrites = data.permissionOverwrites && + data.permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); + + const newData = await this.client.api.channels(this.id).patch({ data: { name: (data.name || this.name).trim(), topic: data.topic, @@ -351,14 +323,14 @@ class GuildChannel extends Channel { user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit, parent_id: data.parentID, lock_permissions: data.lockPermissions, - permission_overwrites: data.permissionOverwrites, + permission_overwrites, }, reason, - }).then(newData => { - const clone = this._clone(); - clone._patch(newData); - return clone; }); + + const clone = this._clone(); + clone._patch(newData); + return clone; } /** diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index ddf9c4208..1d6d70250 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -1,5 +1,7 @@ +const Role = require('./Role'); const Permissions = require('../util/Permissions'); const Util = require('../util/Util'); +const { TypeError } = require('../errors'); /** * Represents a permission overwrite for a role or member in a guild channel. @@ -50,6 +52,27 @@ class PermissionOverwrites { this.allow = new Permissions(data.allow).freeze(); } + /** + * Updates this prermissionOverwrites. + * @param {PermissionOverwriteOption} options The options for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise} + * @example + * // Update permission overwrites + * permissionOverwrites.update({ + * SEND_MESSAGES: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + update(options, reason) { + const { allow, deny } = this.constructor.resolveOverwriteOptions(options, this); + + return this.channel.client.api.channels(this.channel.id).permissions[this.id] + .put({ data: { id: this.id, type: this.type, allow: allow.bitfield, deny: deny.bitfield }, reason }) + .then(() => this); + } + /** * Deletes this Permission Overwrite. * @param {string} [reason] Reason for deleting this overwrite @@ -64,6 +87,102 @@ class PermissionOverwrites { toJSON() { return Util.flatten(this); } + + /** + * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). + * ```js + * { + * 'SEND_MESSAGES': true, + * 'EMBED_LINKS': null, + * 'ATTACH_FILES': false, + * } + * ``` + * @typedef {Object} PermissionOverwriteOption + */ + + /** + * @typedef {object} ResolvedOverwriteOptions + * @property {Permissions} allow The allowed permissions + * @property {Permissions} deny The denied permissions + */ + + /** + * Deletes this Permission Overwrite. + * @param {PermissionOverwriteOption} options The options for the update + * @param {Object} initialPermissions The initial permissions + * @param {PermissionResolvable} initialPermissions.allow Initial allowed permissions + * @param {PermissionResolvable} initialPermissions.deny Initial denied permissions + * @returns {ResolvedOverwriteOptions} + */ + static resolveOverwriteOptions(options, { allow, deny } = {}) { + allow = new Permissions(allow); + deny = new Permissions(deny); + + for (const [perm, value] of Object.entries(options)) { + if (value === true) { + allow.add(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); + } else if (value === false) { + allow.remove(Permissions.FLAGS[perm]); + deny.add(Permissions.FLAGS[perm]); + } else if (value === null) { + allow.remove(Permissions.FLAGS[perm]); + deny.remove(Permissions.FLAGS[perm]); + } + } + + return { allow, deny }; + } + + /** + * The raw data for a permission overwrite + * @typedef {Object} RawOverwriteData + * @property {Snowflake} id The id of the overwrite + * @property {number} allow The permissions to allow + * @property {number} deny The permissions to deny + * @property {OverwriteType} type The type of this OverwriteData + */ + + /** + * Data that can be resolved into {@link RawOverwriteData} + * @typedef {PermissionOverwrites|OverwriteData} OverwriteResolvable + */ + + /** + * Data that can be used for a permission overwrite + * @typedef {Object} OverwriteData + * @property {GuildMemberResolvable|RoleResolvable} id Member or role this overwrite is for + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny + * @property {OverwriteType} [type] The type of this OverwriteData + */ + + /** + * Resolves an overwrite into {@link RawOverwriteData}. + * @param {OverwriteResolvable} overwrite The overwrite-like data to resolve + * @param {Guild} guild The guild to resolve from + * @returns {RawOverwriteData} + */ + static resolve(overwrite, guild) { + if (overwrite instanceof this) return overwrite.toJSON(); + if (typeof overwrite.id === 'string' && ['role', 'member'].includes(overwrite.type)) { + return { ...overwrite, + allow: Permissions.resolve(overwrite.allow), + deny: Permissions.resolve(overwrite.deny), + }; + } + + const userOrRole = guild.roles.resolve(overwrite.id) || guild.client.users.resolve(overwrite.id); + if (!userOrRole) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true); + const type = userOrRole instanceof Role ? 'role' : 'member'; + + return { + id: userOrRole.id, + type, + allow: Permissions.resolve(overwrite.allow), + deny: Permissions.resolve(overwrite.deny), + }; + } } module.exports = PermissionOverwrites; diff --git a/src/structures/shared/resolvePermissions.js b/src/structures/shared/resolvePermissions.js deleted file mode 100644 index c06f83e0f..000000000 --- a/src/structures/shared/resolvePermissions.js +++ /dev/null @@ -1,26 +0,0 @@ -const Permissions = require('../../util/Permissions'); -const Collection = require('../../util/Collection'); - -module.exports = function resolvePermissions(overwrites) { - if (overwrites instanceof Collection || overwrites instanceof Array) { - overwrites = overwrites.map(overwrite => { - const role = this.guild.roles.resolve(overwrite.id); - if (role) { - overwrite.id = role.id; - overwrite.type = 'role'; - } else { - overwrite.id = this.client.users.resolveID(overwrite.id); - overwrite.type = 'member'; - } - - return { - allow: Permissions.resolve(overwrite.allow), - deny: Permissions.resolve(overwrite.deny), - type: overwrite.type, - id: overwrite.id, - }; - }); - } - - return overwrites; -}; diff --git a/src/util/Util.js b/src/util/Util.js index 1098864f9..c413cdd64 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -2,6 +2,7 @@ const { Colors, DefaultOptions, Endpoints } = require('./Constants'); const fetch = require('node-fetch'); const { Error: DiscordError, RangeError, TypeError } = require('../errors'); const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k); +const isObject = d => typeof d === 'object' && d !== null; const { parse } = require('path'); /** @@ -19,7 +20,6 @@ class Util { * @returns {Object} */ static flatten(obj, ...props) { - const isObject = d => typeof d === 'object' && d !== null; if (!isObject(obj)) return obj; props = Object.assign(...Object.keys(obj).filter(k => !k.startsWith('_')).map(k => ({ [k]: true })), ...props); @@ -39,7 +39,7 @@ class Util { // If it's an array, flatten each element else if (Array.isArray(element)) out[newProp] = element.map(e => Util.flatten(e)); // If it's an object with a primitive `valueOf`, use that value - else if (valueOf && !isObject(valueOf)) out[newProp] = valueOf; + else if (typeof valueOf !== 'object') out[newProp] = valueOf; // If it's a primitive else if (!elemIsObj) out[newProp] = element; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 39ddc2974..3042b130c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -519,20 +519,18 @@ declare module 'discord.js' { public rawPosition: number; public clone(options?: GuildChannelCloneOptions): Promise; public createInvite(options?: InviteOptions): Promise; + public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; public edit(data: ChannelData, reason?: string): Promise; public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; public lockPermissions(): Promise; - public overwritePermissions( - options: Array> | Collection>, - reason?: string - ): Promise; + public overwritePermissions(options?: { overwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; public setName(name: string, reason?: string): Promise; public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; public setTopic(topic: string, reason?: string): Promise; - public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: Partial, reason?: string): Promise; + public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } export class GuildEmoji extends Emoji { @@ -786,8 +784,11 @@ declare module 'discord.js' { public deny: Readonly; public id: Snowflake; public type: OverwriteType; + public update(options: PermissionOverwriteOption, reason?: string): Promise; public delete(reason?: string): Promise; public toJSON(): object; + public static resolveOverwriteOptions(options: ResolvedOverwriteOptions, initialPermissions: { allow?: PermissionResolvable, deny?: PermissionResolvable }): ResolvedOverwriteOptions; + public static resolve(overwrite: OverwriteResolvable, guild: Guild): RawOverwriteData; } export class Permissions extends BitField { @@ -1521,7 +1522,7 @@ declare module 'discord.js' { userLimit?: number; parentID?: Snowflake; lockPermissions?: boolean; - permissionOverwrites?: PermissionOverwrites[]; + permissionOverwrites?: OverwriteResolvable[] | Collection; }; type ChannelLogsQueryOptions = { @@ -1738,7 +1739,7 @@ declare module 'discord.js' { bitrate?: number; userLimit?: number; parent?: ChannelResolvable; - overwrites?: (PermissionOverwrites | ChannelCreationOverwrites)[]; + overwrites?: OverwriteResolvable[] | Collection; reason?: string }; @@ -1889,18 +1890,22 @@ declare module 'discord.js' { | 'GUILD_MEMBER_JOIN'; type OverwriteData = { - id: Snowflake; - type: string; - allow?: string; - deny?: string; + allow?: PermissionResolvable; + deny?: PermissionResolvable; + id: GuildMemberResolvable | RoleResolvable; + type?: OverwriteType; }; + type OverwriteResolvable = PermissionOverwrites | OverwriteData; + type OverwriteType = 'member' | 'role'; type PermissionFlags = Record; type PermissionObject = Record; + type PermissionOverwriteOption = { [k in PermissionString]?: boolean | null }; + type PermissionString = 'CREATE_INSTANT_INVITE' | 'KICK_MEMBERS' | 'BAN_MEMBERS' @@ -1964,12 +1969,24 @@ declare module 'discord.js' { route: string; }; + type RawOverwriteData = { + id: Snowflake; + allow: number; + deny: number; + type: OverwriteType; + }; + type ReactionCollectorOptions = CollectorOptions & { max?: number; maxEmojis?: number; maxUsers?: number; }; + type ResolvedOverwriteOptions = { + allow: Permissions; + deny: Permissions; + }; + type RoleData = { name?: string; color?: ColorResolvable; From 53420fa4e7f3ef0e8ee361ed5aec23e02580b0ad Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Fri, 21 Sep 2018 23:02:31 -0500 Subject: [PATCH 0884/1359] fix: Webhook#send not resolving content --- src/structures/Webhook.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index a719249ea..f48a51061 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,6 @@ const DataResolver = require('../util/DataResolver'); const Channel = require('./Channel'); -const APIMessage = require('./APIMessage'); +let APIMessage; /** * Represents a webhook. @@ -15,6 +15,7 @@ class Webhook { */ Object.defineProperty(this, 'client', { value: client }); if (data) this._patch(data); + if (!APIMessage) APIMessage = require('./APIMessage'); } _patch(data) { @@ -128,7 +129,7 @@ class Webhook { async send(content, options) { let apiMessage; - if (content instanceof apiMessage) { + if (content instanceof APIMessage) { apiMessage = content.resolveData(); } else { apiMessage = APIMessage.create(this, content, options).resolveData(); From 3f44320bbe23f5de829fdad0e19f4381f634ef62 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 22 Sep 2018 11:45:03 +0200 Subject: [PATCH 0885/1359] docs(MessageAttachment): redocument name property --- src/structures/MessageAttachment.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 90ae4bbc2..cce6e487b 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -11,6 +11,10 @@ class MessageAttachment { */ constructor(attachment, name = null, data) { this.attachment = attachment; + /** + * The name of this attachment + * @type {?string} + */ this.name = name; if (data) this._patch(data); } From c0a9b08e73ffc2813e4dd6881d408f2c743242fc Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 23 Sep 2018 17:02:35 +0200 Subject: [PATCH 0886/1359] fix(Webhook): APIMessage is not circular, fixes WebhookClients --- src/structures/Webhook.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index f48a51061..5b1328ec2 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,6 @@ const DataResolver = require('../util/DataResolver'); const Channel = require('./Channel'); -let APIMessage; +let APIMessage = require('./APIMessage'); /** * Represents a webhook. @@ -15,7 +15,6 @@ class Webhook { */ Object.defineProperty(this, 'client', { value: client }); if (data) this._patch(data); - if (!APIMessage) APIMessage = require('./APIMessage'); } _patch(data) { From 0ab69c5ad4ef6a9fe4070da4ec13b024ca17043a Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 23 Sep 2018 17:04:22 +0200 Subject: [PATCH 0887/1359] fix(Webhook): APIMessage require should be a const --- 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 5b1328ec2..c12cc67f8 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,6 @@ const DataResolver = require('../util/DataResolver'); const Channel = require('./Channel'); -let APIMessage = require('./APIMessage'); +const APIMessage = require('./APIMessage'); /** * Represents a webhook. From 3298ea186992d1574b5595b93d97e0e957851b97 Mon Sep 17 00:00:00 2001 From: Andrew Lehman Date: Sun, 23 Sep 2018 15:35:56 -0400 Subject: [PATCH 0888/1359] docs: correct default value for SplitOptions.maxLength in Util#splitMessage (#2847) --- src/structures/interfaces/TextBasedChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 7f9553f1e..f9c668de3 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -73,7 +73,7 @@ class TextBasedChannel { /** * Options for splitting a message. * @typedef {Object} SplitOptions - * @property {number} [maxLength=1950] Maximum character length per message piece + * @property {number} [maxLength=2000] Maximum character length per message piece * @property {string} [char='\n'] Character to split the message with * @property {string} [prepend=''] Text to prepend to every piece except the first * @property {string} [append=''] Text to append to every piece except the last From 32b405a5bebb9f2b034872e4e7be14b9e55f0f56 Mon Sep 17 00:00:00 2001 From: Shayne Hartford Date: Sun, 23 Sep 2018 15:36:28 -0400 Subject: [PATCH 0889/1359] Change default token (#2843) * Change default token Default `token` to `process.env.CLIENT_TOKEN` like client does. * 2/2 * 1/2 --- src/client/Client.js | 4 ++-- src/sharding/ShardingManager.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index d55b2d88e..99da4996a 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -104,13 +104,13 @@ class Client extends BaseClient { this.presence = new ClientPresence(this); Object.defineProperty(this, 'token', { writable: true }); - if (!browser && !this.token && 'CLIENT_TOKEN' in process.env) { + if (!browser && !this.token && 'DISCORD_TOKEN' in process.env) { /** * Authorization token for the logged in bot * This should be kept private at all times. * @type {?string} */ - this.token = process.env.CLIENT_TOKEN; + this.token = process.env.DISCORD_TOKEN; } else { this.token = null; } diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 2b96ab2dd..bbec15b67 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -31,7 +31,7 @@ class ShardingManager extends EventEmitter { totalShards: 'auto', respawn: true, shardArgs: [], - token: null, + token: process.env.DISCORD_TOKEN, }, options); /** From 60ad9053a3f7d0cfcfe9871bc5aa6eca791caf02 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sun, 23 Sep 2018 22:07:19 +0200 Subject: [PATCH 0890/1359] typings: add HTTPError class definition (#2852) Class was introduced with #2694 * typings: Add HTTPError class definition * typings: Sort HTTPError's members by name --- typings/index.d.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 3042b130c..80ba73526 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -604,6 +604,14 @@ declare module 'discord.js' { public sync(): Promise; } + export class HTTPError extends Error { + constructor(message: string, name: string, code: number, method: string, path: string); + public code: number; + public method: string; + public name: string; + public path: string; + } + export class Invite extends Base { constructor(client: Client, data: object); public channel: GuildChannel | GroupDMChannel; From d4c0bb2a0d9a103a6c8242439c918704b93ead85 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 29 Sep 2018 16:02:49 +0200 Subject: [PATCH 0891/1359] feat(TextChannel): RateLimitPerUser (#2811) * feat: Add TextChannel#rateLimitPerUser Rename parameter in TextChannel#setRateLimitPerUser feat: Add `rateLimitPerUser` param to ChannelData fix: eslint * docs: Updated typings * fix: Requested changes * fix: rateLimitPerUser being undefined when 0 When `rate_limit_per_user` is 0, the gateway does not send it (but REST does). When this is set to a non-zero number, this property starts to exist. Otherwise this will be `0`. Adding `|| 0` should do the trick changing `undefined` to `0`. * fix: eslint --- src/structures/GuildChannel.js | 2 ++ src/structures/TextChannel.js | 16 ++++++++++++++++ typings/index.d.ts | 3 +++ 3 files changed, 21 insertions(+) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 0acf59676..242b6f048 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -286,6 +286,7 @@ class GuildChannel extends Channel { * Lock the permissions of the channel to what the parent's permissions are * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] * Permission overwrites for the channel + * @property {number} [rateLimitPerUser] The ratelimit per user for the channel */ /** @@ -323,6 +324,7 @@ class GuildChannel extends Channel { user_limit: typeof data.userLimit !== 'undefined' ? data.userLimit : this.userLimit, parent_id: data.parentID, lock_permissions: data.lockPermissions, + rate_limit_per_user: data.rateLimitPerUser, permission_overwrites, }, reason, diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 698e23f20..6e053e5d0 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -43,6 +43,12 @@ class TextChannel extends GuildChannel { */ this.lastMessageID = data.last_message_id; + /** + * The ratelimit per user for this channel + * @type {number} + */ + this.rateLimitPerUser = data.rate_limit_per_user || 0; + /** * The timestamp when the last pinned message was pinned, if there was one * @type {?number} @@ -52,6 +58,16 @@ class TextChannel extends GuildChannel { if (data.messages) for (const message of data.messages) this.messages.add(message); } + /** + * Sets the rate limit per user for this channel. + * @param {number} rateLimitPerUser The new ratelimit + * @param {string} [reason] Reason for changing the channel's ratelimits + * @returns {Promise} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser }, reason); + } + /** * Sets whether this channel is flagged as NSFW. * @param {boolean} nsfw Whether the channel should be considered NSFW diff --git a/typings/index.d.ts b/typings/index.d.ts index 80ba73526..174510d81 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1045,9 +1045,11 @@ declare module 'discord.js' { public readonly members: Collection; public messages: MessageStore; public nsfw: boolean; + public rateLimitPerUser: number; public topic: string; public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; + public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; public fetchWebhooks(): Promise>; } @@ -1529,6 +1531,7 @@ declare module 'discord.js' { bitrate?: number; userLimit?: number; parentID?: Snowflake; + rateLimitPerUser?: number; lockPermissions?: boolean; permissionOverwrites?: OverwriteResolvable[] | Collection; }; From 21999fa4a0f392ebc7ac8e5a5c1aa73879e0d49d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 30 Sep 2018 11:30:07 +0200 Subject: [PATCH 0892/1359] fix(typings): fix compilation with 'strict' enabled (#2845) --- typings/index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 174510d81..d5f8dcf58 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -37,7 +37,7 @@ declare module 'discord.js' { export class ActivityFlags extends BitField { public static FLAGS: Record; - public static resolve(permission: BitFieldResolvable): number; + public static resolve(bit?: BitFieldResolvable): number; } export class APIMessage { @@ -113,7 +113,7 @@ declare module 'discord.js' { public valueOf(): number; public [Symbol.iterator](): Iterator; public static FLAGS: object; - public static resolve(bit?: BitFieldResolvable): number; + public static resolve(bit?: BitFieldResolvable): number; } export class CategoryChannel extends GuildChannel { @@ -1030,7 +1030,7 @@ declare module 'discord.js' { export class Speaking extends BitField { public static FLAGS: Record; - public static resolve(permission: BitFieldResolvable): number; + public static resolve(bit?: BitFieldResolvable): number; } export class Structures { From d8c97be142548b934fb3067bf84ff6eb10146906 Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 1 Oct 2018 05:38:36 -0500 Subject: [PATCH 0893/1359] docs: fix typos (#2868) --- src/client/voice/networking/VoiceWebSocket.js | 2 +- src/client/websocket/WebSocketConnection.js | 2 +- src/client/websocket/packets/handlers/ChannelPinsUpdate.js | 2 +- src/structures/Guild.js | 2 +- src/structures/PermissionOverwrites.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 5e0608ced..d33d5fbe7 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -216,7 +216,7 @@ class VoiceWebSocket extends EventEmitter { } if (this.heartbeatInterval) { /** - * Emitted whenver the voice WebSocket encounters a non-fatal error. + * Emitted whenever the voice WebSocket encounters a non-fatal error. * @param {string} warn The warning * @event VoiceWebSocket#warn */ diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketConnection.js index fdf58eb5c..12e90075c 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketConnection.js @@ -337,7 +337,7 @@ class WebSocketConnection extends EventEmitter { * Causes a reconnection to the gateway. */ reconnect() { - this.debug('Attemping to reconnect in 5500ms...'); + this.debug('Attempting to reconnect in 5500ms...'); /** * Emitted whenever the client tries to reconnect to the WebSocket. * @event Client#reconnecting diff --git a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js index 702be2253..b8cb64019 100644 --- a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js +++ b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js @@ -32,6 +32,6 @@ module.exports = ChannelPinsUpdate; * can be provided easily here - you need to manually check the pins yourself. * The `time` parameter will be a Unix Epoch Date object when there are no pins left. * @event Client#channelPinsUpdate - * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in + * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occurred in * @param {Date} time The time when the last pinned message was pinned */ diff --git a/src/structures/Guild.js b/src/structures/Guild.js index d24b3c175..493c0df81 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -435,7 +435,7 @@ class Guild extends Base { /** * Creates an integration by attaching an integration object - * @param {IntegrationData} data The data for thes integration + * @param {IntegrationData} data The data for the integration * @param {string} reason Reason for creating the integration * @returns {Promise} */ diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 1d6d70250..b28517864 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -53,7 +53,7 @@ class PermissionOverwrites { } /** - * Updates this prermissionOverwrites. + * Updates this permissionOverwrites. * @param {PermissionOverwriteOption} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} From 96a065560990a5fd06e7bdf38c62502dcba71b9a Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 1 Oct 2018 05:43:08 -0500 Subject: [PATCH 0894/1359] refactor(WebhookClient): make token property non-enumerable (#2861) * make WebhookClient#token non-enumerable parity with Client to add the barest protection against accidental exposure * requested change --- src/client/WebhookClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index c4c297879..0a848d808 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -20,7 +20,7 @@ class WebhookClient extends BaseClient { super(options); Object.defineProperty(this, 'client', { value: this }); this.id = id; - this.token = token; + Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true }); } } From 62e7e26310927d2898eca5c5036d29fd5e79fcc0 Mon Sep 17 00:00:00 2001 From: Daniel <24366082+danielgulic@users.noreply.github.com> Date: Mon, 1 Oct 2018 22:02:50 +1000 Subject: [PATCH 0895/1359] Add white to colours (#2853) * add white to colorresolvable typedef * add colour white --- src/util/Constants.js | 1 + src/util/Util.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index c4ab29a93..32413c257 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -382,6 +382,7 @@ exports.ClientApplicationAssetTypes = { exports.Colors = { DEFAULT: 0x000000, + WHITE: 0xFFFFFF, AQUA: 0x1ABC9C, GREEN: 0x2ECC71, BLUE: 0x3498DB, diff --git a/src/util/Util.js b/src/util/Util.js index c413cdd64..119d0c7c7 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -245,6 +245,7 @@ class Util { * ``` * or one of the following strings: * - `DEFAULT` + * - `WHITE` * - `AQUA` * - `GREEN` * - `BLUE` From 142896723886dadfe92aedfccc1fd0a36b492376 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 4 Oct 2018 11:01:21 +0200 Subject: [PATCH 0896/1359] docs(Integration): document Integration class --- src/structures/Integration.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/structures/Integration.js b/src/structures/Integration.js index 359df9eb3..2782008ba 100644 --- a/src/structures/Integration.js +++ b/src/structures/Integration.js @@ -7,6 +7,9 @@ const Base = require('./Base'); * @property {string} name The name of the account */ +/** + * Represents a guild integration. + */ class Integration extends Base { constructor(client, data, guild) { super(client); From 6b810b23312d0fbb378744772ca45303cdc320b3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 4 Oct 2018 12:36:25 +0200 Subject: [PATCH 0897/1359] fix(ClientPresence): use possibly extended constructor from structures --- src/client/Client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index 99da4996a..b87d21c94 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -14,10 +14,10 @@ const VoiceBroadcast = require('./voice/VoiceBroadcast'); const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); -const ClientPresence = require('../structures/ClientPresence'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); const { Events, browser } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); +const Structures = require('../util/Structures'); const { Error, TypeError, RangeError } = require('../errors'); /** @@ -96,6 +96,7 @@ class Client extends BaseClient { */ this.channels = new ChannelStore(this); + const ClientPresence = Structures.get('ClientPresence'); /** * The presence of the Client * @private From 00a62a93e4b36ea3f31d31e8a9e966137c3f77a8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 4 Oct 2018 20:57:52 +0100 Subject: [PATCH 0898/1359] voice: catch errors before connection is ready --- src/client/voice/ClientVoiceManager.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index cca3b15ad..d55a809f2 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -71,8 +71,11 @@ class ClientVoiceManager { }); connection.once('authenticated', () => { - connection.once('ready', () => resolve(connection)); - connection.once('error', reject); + connection.once('ready', () => { + resolve(connection); + connection.removeListener('error', reject); + }); + connection.on('error', reject); connection.once('disconnect', () => this.connections.delete(channel.guild.id)); }); }); From 27d2ce7baf9a25d2e5eec76cd66b8ca14aabd5ee Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 Oct 2018 11:37:49 +0200 Subject: [PATCH 0899/1359] typings(Collection): each return Collection, not void --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index d5f8dcf58..ad9baa67e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -279,7 +279,7 @@ declare module 'discord.js' { public array(): V[]; public clone(): Collection; public concat(...collections: Collection[]): Collection; - public each(fn: (value: V, key: K, collection: Collection) => void, thisArg?: any): void; + public each(fn: (value: V, key: K, collection: Collection) => void, thisArg?: any): Collection; public equals(collection: Collection): boolean; public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; From 1fe36087d4a665fc61905a84e69f2257437a1a72 Mon Sep 17 00:00:00 2001 From: Isabella Date: Wed, 10 Oct 2018 02:57:13 -0500 Subject: [PATCH 0900/1359] feat(MessageEmbed): allow setTimestamp to take a timestamp (#2875) * feat: allow MessageEmbed#setTimestamp to take a timestamp * fix the dumb i did --- src/structures/MessageEmbed.js | 7 ++++--- typings/index.d.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 2d0208b24..5d2b2066f 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -267,11 +267,12 @@ class MessageEmbed { /** * Sets the timestamp of this embed. - * @param {Date} [timestamp=current date] The timestamp + * @param {Date|number} [timestamp=Date.now()] The timestamp or date * @returns {MessageEmbed} */ - setTimestamp(timestamp = new Date()) { - this.timestamp = timestamp.getTime(); + setTimestamp(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + this.timestamp = timestamp; return this; } diff --git a/typings/index.d.ts b/typings/index.d.ts index ad9baa67e..8e66679b7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -741,7 +741,7 @@ declare module 'discord.js' { public setFooter(text: StringResolvable, iconURL?: string): this; public setImage(url: string): this; public setThumbnail(url: string): this; - public setTimestamp(timestamp?: Date): this; + public setTimestamp(timestamp?: Date | number): this; public setTitle(title: StringResolvable): this; public setURL(url: string): this; public toJSON(): object; From 8feb87458645a27c103dcae9eed9803e976890f7 Mon Sep 17 00:00:00 2001 From: bdistin Date: Wed, 10 Oct 2018 02:57:56 -0500 Subject: [PATCH 0901/1359] feat(MessageEmbed): add spliceField method (#2857) * Add spliceField and refactor to prevent code dupe * String() was for a falsy check, fixed * requested: remove embed field count checks --- src/errors/Messages.js | 1 - src/structures/MessageEmbed.js | 52 +++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 80fa7e389..2d1bc3c2c 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -29,7 +29,6 @@ const Messages = { COLOR_RANGE: 'Color must be within the range 0 - 16777215 (0xFFFFFF).', COLOR_CONVERT: 'Unable to convert color to a number.', - EMBED_FIELD_COUNT: 'MessageEmbeds may not exceed 25 fields.', EMBED_FIELD_NAME: 'MessageEmbed field names may not be empty.', EMBED_FIELD_VALUE: 'MessageEmbed field values may not be empty.', diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 5d2b2066f..49ddf30a7 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -51,12 +51,16 @@ class MessageEmbed { this.timestamp = data.timestamp ? new Date(data.timestamp).getTime() : null; /** - * The fields of this embed - * @type {Object[]} + * @typedef {Object} EmbedField * @property {string} name The name of this field * @property {string} value The value of this field * @property {boolean} inline If this field will be displayed inline */ + + /** + * The fields of this embed + * @type {EmbedField[]} + */ this.fields = data.fields ? data.fields.map(Util.cloneObject) : []; /** @@ -170,13 +174,8 @@ class MessageEmbed { * @param {boolean} [inline=false] Set the field to display inline * @returns {MessageEmbed} */ - addField(name, value, inline = false) { - if (this.fields.length >= 25) throw new RangeError('EMBED_FIELD_COUNT'); - name = Util.resolveString(name); - if (!String(name)) throw new RangeError('EMBED_FIELD_NAME'); - value = Util.resolveString(value); - if (!String(value)) throw new RangeError('EMBED_FIELD_VALUE'); - this.fields.push({ name, value, inline }); + addField(name, value, inline) { + this.fields.push(this.constructor.checkField(name, value, inline)); return this; } @@ -185,10 +184,28 @@ class MessageEmbed { * @param {boolean} [inline=false] Set the field to display inline * @returns {MessageEmbed} */ - addBlankField(inline = false) { + addBlankField(inline) { return this.addField('\u200B', '\u200B', inline); } + /** + * Removes, replaces, and inserts fields in the embed (max 25). + * @param {number} index The index to start at + * @param {number} deleteCount The number of fields to remove + * @param {StringResolvable} [name] The name of the field + * @param {StringResolvable} [value] The value of the field + * @param {boolean} [inline=false] Set the field to display inline + * @returns {MessageEmbed} + */ + spliceField(index, deleteCount, name, value, inline) { + if (name && value) { + this.fields.splice(index, deleteCount, this.constructor.checkField(name, value, inline)); + } else { + this.fields.splice(index, deleteCount); + } + return this; + } + /** * Sets the file to upload alongside the embed. This file can be accessed via `attachment://fileName.extension` when * setting an embed image or author/footer icons. Multiple files can be attached. @@ -328,6 +345,21 @@ class MessageEmbed { } : null, }; } + + /** + * Checks for valid field input and resolves strings + * @param {StringResolvable} name The name of the field + * @param {StringResolvable} value The value of the field + * @param {boolean} [inline=false] Set the field to display inline + * @returns {EmbedField} + */ + static checkField(name, value, inline = false) { + name = Util.resolveString(name); + if (!name) throw new RangeError('EMBED_FIELD_NAME'); + value = Util.resolveString(value); + if (!value) throw new RangeError('EMBED_FIELD_VALUE'); + return { name, value, inline }; + } } module.exports = MessageEmbed; From 8ec3b5134dcd55c5d18910ad0d751ef0c01568d6 Mon Sep 17 00:00:00 2001 From: Dim Date: Wed, 10 Oct 2018 03:58:31 -0400 Subject: [PATCH 0902/1359] feat(GuildChannelStore): support for create with rateLimitPerUser (#2878) * change GuildChannelStore#create * document rateLimitPerUser * update typings --- src/stores/GuildChannelStore.js | 4 +++- typings/index.d.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 1532f5920..411734bd0 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -33,6 +33,7 @@ class GuildChannelStore extends DataStore { * @param {ChannelResolvable} [options.parent] Parent of the new channel * @param {OverwriteResolvable[]|Collection} [options.overwrites] * Permission overwrites of the new channel + * @param {number} [options.rateLimitPerUser] The ratelimit per user for the channel * @param {string} [options.reason] Reason for creating the channel * @returns {Promise} * @example @@ -52,7 +53,7 @@ class GuildChannelStore extends DataStore { * ], * }) */ - async create(name, { type, topic, nsfw, bitrate, userLimit, parent, overwrites, reason } = {}) { + async create(name, { type, topic, nsfw, bitrate, userLimit, parent, overwrites, rateLimitPerUser, reason } = {}) { if (parent) parent = this.client.channels.resolveID(parent); const data = await this.client.api.guilds(this.guild.id).channels.post({ @@ -65,6 +66,7 @@ class GuildChannelStore extends DataStore { user_limit: userLimit, parent_id: parent, permission_overwrites: overwrites && overwrites.map(o => PermissionOverwrites.resolve(o, this.guild)), + rate_limit_per_user: rateLimitPerUser, }, reason, }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 8e66679b7..6a2fe4bf1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1751,6 +1751,7 @@ declare module 'discord.js' { userLimit?: number; parent?: ChannelResolvable; overwrites?: OverwriteResolvable[] | Collection; + rateLimitPerUser?: number; reason?: string }; From 1ee417cd65e81c4c44669355baf3de717b5b8b3a Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Wed, 10 Oct 2018 00:59:16 -0700 Subject: [PATCH 0903/1359] featt(ClientUser): allow options as first parameter to setActivity (#2890) --- src/structures/ClientPresence.js | 2 +- src/structures/ClientUser.js | 24 +++++++++++++++--------- typings/index.d.ts | 9 ++++++++- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index adba78f3e..90d1cb4a2 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -8,7 +8,7 @@ class ClientPresence extends Presence { super(client, Object.assign(data, { status: 'online', user: { id: null } })); } - async setClientPresence(presence) { + async set(presence) { const packet = await this._parse(presence); this.patch(packet); this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet }); diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 8d35765d9..05d0794f8 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -97,7 +97,7 @@ class ClientUser extends Structures.get('User') { * .catch(console.error); */ setPresence(data) { - return this.client.presence.setClientPresence(data); + return this.client.presence.set(data); } /** @@ -123,12 +123,18 @@ class ClientUser extends Structures.get('User') { return this.setPresence({ status }); } + /** + * Options for setting an activity + * @typedef ActivityOptions + * @type {Object} + * @property {string} [url] Twitch stream URL + * @property {ActivityType|number} [type] Type of the activity + */ + /** * Sets the activity the client user is playing. - * @param {?string} name Activity being played - * @param {Object} [options] Options for setting the activity - * @param {string} [options.url] Twitch stream URL - * @param {ActivityType|number} [options.type] Type of the activity + * @param {string|ActivityOptions} [name] Activity being played, or options for setting the activity + * @param {ActivityOptions} [options] Options for setting the activity * @returns {Promise} * @example * // Set the client user's activity @@ -136,11 +142,11 @@ class ClientUser extends Structures.get('User') { * .then(presence => console.log(`Activity set to ${presence.game.name}`)) * .catch(console.error); */ - setActivity(name, { url, type } = {}) { + setActivity(name, options = {}) { if (!name) return this.setPresence({ activity: null }); - return this.setPresence({ - activity: { name, type, url }, - }); + + const activity = Object.assign({}, options, typeof name === 'object' ? name : { name }); + return this.setPresence({ activity }); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 6a2fe4bf1..2a570ec57 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -260,11 +260,18 @@ declare module 'discord.js' { public connectToWebSocket(token: string, resolve: Function, reject: Function): void; } + export interface ActivityOptions { + name?: string; + url?: string; + type?: ActivityType | number; + } + export class ClientUser extends User { public mfaEnabled: boolean; public verified: boolean; public createGroupDM(recipients: GroupDMRecipientOptions[]): Promise; - public setActivity(name: string, options?: { url?: string, type?: ActivityType | number }): Promise; + public setActivity(options?: ActivityOptions): Promise; + public setActivity(name: string, options?: ActivityOptions): Promise; public setAFK(afk: boolean): Promise; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; public setPresence(data: PresenceData): Promise; From 71c04a303a2c77e908d7c6f2ba742b520a2e1e73 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 10 Oct 2018 10:01:23 +0200 Subject: [PATCH 0904/1359] feat(Emoji): move createdAt and createdTimestamp getters from GuildEmoji (#2872) --- src/structures/Emoji.js | 22 ++++++++++++++++++++++ src/structures/GuildEmoji.js | 19 ------------------- typings/index.d.ts | 4 ++-- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 20d6b12bb..ed844f18a 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -1,5 +1,7 @@ +const Snowflake = require('../util/Snowflake'); const Base = require('./Base'); + /** * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}. * @extends {Base} @@ -52,6 +54,26 @@ class Emoji extends Base { return this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png'); } + /** + * The timestamp the emoji was created at, or null if unicode + * @type {?number} + * @readonly + */ + get createdTimestamp() { + if (!this.id) return null; + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the emoji was created at, or null if unicode + * @type {?Date} + * @readonly + */ + get createdAt() { + if (!this.id) return null; + return new Date(this.createdTimestamp); + } + /** * When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord * instead of the Emoji object. diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 20e48aa06..93e8d275d 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,7 +1,6 @@ const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore'); const Permissions = require('../util/Permissions'); const { Error } = require('../errors'); -const Snowflake = require('../util/Snowflake'); const Emoji = require('./Emoji'); /** @@ -65,24 +64,6 @@ class GuildEmoji extends Emoji { return new GuildEmojiRoleStore(this); } - /** - * The timestamp the emoji was created at - * @type {number} - * @readonly - */ - get createdTimestamp() { - return Snowflake.deconstruct(this.id).timestamp; - } - - /** - * The time the emoji was created at - * @type {Date} - * @readonly - */ - get createdAt() { - return new Date(this.createdTimestamp); - } - /** * Fetches the author for this emoji * @returns {Promise} diff --git a/typings/index.d.ts b/typings/index.d.ts index 2a570ec57..ca77f6438 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -370,6 +370,8 @@ declare module 'discord.js' { export class Emoji extends Base { constructor(client: Client, emoji: object); public animated: boolean; + public readonly createdAt: Date; + public readonly createdTimestamp: number; public readonly deletable: boolean; public id: Snowflake; public name: string; @@ -544,8 +546,6 @@ declare module 'discord.js' { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; - public readonly createdAt: Date; - public readonly createdTimestamp: number; public deleted: boolean; public guild: Guild; public managed: boolean; From 6e5f088e447548f0b4ef7d16680b877e14bc93aa Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 10 Oct 2018 10:25:00 +0200 Subject: [PATCH 0905/1359] refactor: consistently use permissionOverwrites over overwrites (#2886) --- src/stores/GuildChannelStore.js | 14 +++++++++----- src/structures/GuildChannel.js | 17 ++++++++--------- typings/index.d.ts | 4 ++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 411734bd0..e67bcc1c3 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -31,7 +31,7 @@ class GuildChannelStore extends DataStore { * @param {number} [options.bitrate] Bitrate of the new channel in bits (only voice) * @param {number} [options.userLimit] Maximum amount of users allowed in the new channel (only voice) * @param {ChannelResolvable} [options.parent] Parent of the new channel - * @param {OverwriteResolvable[]|Collection} [options.overwrites] + * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites] * Permission overwrites of the new channel * @param {number} [options.rateLimitPerUser] The ratelimit per user for the channel * @param {string} [options.reason] Reason for creating the channel @@ -42,10 +42,10 @@ class GuildChannelStore extends DataStore { * .then(console.log) * .catch(console.error); * @example - * // Create a new channel with overwrites + * // Create a new channel with permission overwrites * guild.channels.create('new-voice', { * type: 'voice', - * overwrites: [ + * permissionOverwrites: [ * { * id: message.author.id, * deny: ['VIEW_CHANNEL'], @@ -53,8 +53,12 @@ class GuildChannelStore extends DataStore { * ], * }) */ - async create(name, { type, topic, nsfw, bitrate, userLimit, parent, overwrites, rateLimitPerUser, reason } = {}) { + async create(name, options = {}) { + let { type, topic, nsfw, bitrate, userLimit, parent, permissionOverwrites, rateLimitPerUser, reason } = options; if (parent) parent = this.client.channels.resolveID(parent); + if (permissionOverwrites) { + permissionOverwrites = permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); + } const data = await this.client.api.guilds(this.guild.id).channels.post({ data: { @@ -65,7 +69,7 @@ class GuildChannelStore extends DataStore { bitrate, user_limit: userLimit, parent_id: parent, - permission_overwrites: overwrites && overwrites.map(o => PermissionOverwrites.resolve(o, this.guild)), + permission_overwrites: permissionOverwrites, rate_limit_per_user: rateLimitPerUser, }, reason, diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 242b6f048..3567ea4f9 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -178,13 +178,13 @@ class GuildChannel extends Channel { /** * Replaces the permission overwrites in this channel. * @param {Object} [options] Options - * @param {OverwriteResolvable[]|Collection} [options.overwrites] + * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites] * Permission overwrites the channel gets updated with * @param {string} [options.reason] Reason for updating the channel overwrites * @returns {Promise} * @example * channel.overwritePermissions({ - * overwrites: [ + * permissionOverwrites: [ * { * id: message.author.id, * deny: ['VIEW_CHANNEL'], @@ -193,9 +193,8 @@ class GuildChannel extends Channel { * reason: 'Needed to change permissions' * }); */ - overwritePermissions({ overwrites, reason } = {}) { - return this.edit({ permissionOverwrites: overwrites, reason }) - .then(() => this); + overwritePermissions(options = {}) { + return this.edit(options).then(() => this); } /** @@ -465,10 +464,11 @@ class GuildChannel extends Channel { */ clone(options = {}) { if (typeof options.withPermissions === 'undefined') options.withPermissions = true; + if (typeof options.withTopic === 'undefined') options.withTopic = true; Util.mergeDefault({ name: this.name, - overwrites: options.withPermissions ? this.permissionOverwrites : [], - withTopic: true, + permissionOverwrites: options.withPermissions ? this.permissionOverwrites : [], + topic: options.withTopic ? this.topic : undefined, nsfw: this.nsfw, parent: this.parent, bitrate: this.bitrate, @@ -476,8 +476,7 @@ class GuildChannel extends Channel { reason: null, }, options); options.type = this.type; - return this.guild.channels.create(options.name, options) - .then(channel => options.withTopic ? channel.setTopic(this.topic) : channel); + return this.guild.channels.create(options.name, options); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index ca77f6438..1163c61c4 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -533,7 +533,7 @@ declare module 'discord.js' { public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; public lockPermissions(): Promise; - public overwritePermissions(options?: { overwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; + public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; public setName(name: string, reason?: string): Promise; public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; @@ -1757,7 +1757,7 @@ declare module 'discord.js' { bitrate?: number; userLimit?: number; parent?: ChannelResolvable; - overwrites?: OverwriteResolvable[] | Collection; + permissionOverwrites?: OverwriteResolvable[] | Collection; rateLimitPerUser?: number; reason?: string }; From 78c4be52c60ea6fa2bb3b34d28562078e0b32082 Mon Sep 17 00:00:00 2001 From: Souji Date: Wed, 10 Oct 2018 10:29:53 +0200 Subject: [PATCH 0906/1359] docs(Collection): clarify example for tap method (#2881) * docs: clarify example for Collection#tap * docs: wording improvement --- src/util/Collection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Collection.js b/src/util/Collection.js index 887d999b1..7f0570d6c 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -349,9 +349,9 @@ class Collection extends Map { * @returns {Collection} * @example * collection - * .tap(coll => coll.size) + * .tap(coll => console.log(`${coll.size} users, including bots`)) * .filter(user => user.bot) - * .tap(coll => coll.size) + * .tap(coll => console.log(`${coll.size} users, excluding bots`)) */ tap(fn, thisArg) { if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); From fd25d19c9c7dbdba745f5bbe5c08d59e5717b74d Mon Sep 17 00:00:00 2001 From: Souji Date: Wed, 10 Oct 2018 10:30:23 +0200 Subject: [PATCH 0907/1359] docs(ClientUser): fix example for setActivity method (#2880) --- src/structures/ClientUser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 05d0794f8..9827e16de 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -139,7 +139,7 @@ class ClientUser extends Structures.get('User') { * @example * // Set the client user's activity * client.user.setActivity('discord.js', { type: 'WATCHING' }) - * .then(presence => console.log(`Activity set to ${presence.game.name}`)) + * .then(presence => console.log(`Activity set to ${presence.activity.name}`)) * .catch(console.error); */ setActivity(name, options = {}) { From 4491b7b42ae19ca81338736d811f3b12d493a5f3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 10 Oct 2018 11:30:26 +0200 Subject: [PATCH 0908/1359] typings(MessageEmbed): add spliceField and static checkField methods --- typings/index.d.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 1163c61c4..6491a4703 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -727,7 +727,7 @@ declare module 'discord.js' { public color: number; public readonly createdAt: Date; public description: string; - public fields: { name: string; value: string; inline?: boolean; }[]; + public fields: EmbedField[]; public files: (MessageAttachment | string | FileOptions)[]; public footer: { text?: string; iconURL?: string; proxyIconURL?: string }; public readonly hexColor: string; @@ -751,7 +751,10 @@ declare module 'discord.js' { public setTimestamp(timestamp?: Date | number): this; public setTitle(title: StringResolvable): this; public setURL(url: string): this; + public spliceField(index: number, deleteCount: number, name?: StringResolvable, value?: StringResolvable, inline?: boolean): this; public toJSON(): object; + + public static checkField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; } export class MessageMentions { @@ -1630,6 +1633,8 @@ declare module 'discord.js' { roles?: Collection | RoleResolvable[]; }; + type EmbedField = { name: string, value: string, inline?: boolean }; + type EmojiIdentifierResolvable = string | EmojiResolvable; type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; From 2e7094f9eae39a0bc61fcf25c7d3362cd80531dd Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 11 Oct 2018 23:02:59 +0100 Subject: [PATCH 0909/1359] voice: fix disconnect without receiver edge case --- src/client/voice/networking/VoiceWebSocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index d33d5fbe7..bd03ea54a 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -180,7 +180,7 @@ class VoiceWebSocket extends EventEmitter { this.connection.ssrcMap.set(+packet.d.audio_ssrc, packet.d.user_id); break; case VoiceOPCodes.CLIENT_DISCONNECT: - const streamInfo = this.connection.receiver.packets.streams.get(packet.d.user_id); + const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id); if (streamInfo) { this.connection.receiver.packets.streams.delete(packet.d.user_id); streamInfo.stream.push(null); From 73b9b0e62b46144d0a0e834f600f53a7efbe27ed Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 13 Oct 2018 15:20:56 +0200 Subject: [PATCH 0910/1359] fix(Shard): use DISCORD_TOKEN instead of CLIENT_TOKEN, see #2843 --- src/sharding/Shard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index a7aa868cb..1fb479752 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -50,7 +50,7 @@ class Shard extends EventEmitter { SHARDING_MANAGER: true, SHARD_ID: this.id, SHARD_COUNT: this.manager.totalShards, - CLIENT_TOKEN: this.manager.token, + DISCORD_TOKEN: this.manager.token, }); /** From 9cf50d05f2196fd01e5376642c0ab083e87512f7 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sat, 13 Oct 2018 20:09:19 -0500 Subject: [PATCH 0911/1359] fix(typings): ReactionUserStore#fetch returns a Collection fixes #2895 --- src/stores/ReactionUserStore.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js index dace1e94c..1b0275795 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/stores/ReactionUserStore.js @@ -18,7 +18,7 @@ class ReactionUserStore extends DataStore { * @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>} + * @returns {Promise>} */ async fetch({ limit = 100, after, before } = {}) { const message = this.reaction.message; diff --git a/typings/index.d.ts b/typings/index.d.ts index 6491a4703..f431c3d19 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1372,7 +1372,7 @@ declare module 'discord.js' { export class ReactionUserStore extends DataStore { constructor(client: Client, iterable: Iterable | undefined, reaction: MessageReaction); - public fetch(options?: { limit?: number, after?: Snowflake, before?: Snowflake }): Promise; + public fetch(options?: { limit?: number, after?: Snowflake, before?: Snowflake }): Promise>; public remove(user?: UserResolvable): Promise; } From 183ba25fafa51b76ea38d77235f9d33dd2f02daa Mon Sep 17 00:00:00 2001 From: Isabella Date: Sun, 14 Oct 2018 12:44:14 -0500 Subject: [PATCH 0912/1359] fix: emit voiceStateUpdate on guildMemberRemove (#2892) * fix: emit voiceStateUpdate on guildMemberRemove * apparently i am blind * typings --- .../websocket/packets/handlers/VoiceStateUpdate.js | 10 ++++++---- typings/index.d.ts | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js index 57d1cf8b8..d6cc08aee 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -26,8 +26,10 @@ class VoiceStateUpdateHandler extends AbstractHandler { } // Emit event - if (member) { - if (member.user.id === client.user.id && data.channel_id) client.emit('self.voiceStateUpdate', data); + if (member && member.user.id === client.user.id && data.channel_id) { + client.emit('self.voiceStateUpdate', data); + } + if (oldState || newState) { client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); } } @@ -37,8 +39,8 @@ class VoiceStateUpdateHandler extends AbstractHandler { /** * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate - * @param {VoiceState} oldState The voice state before the update - * @param {VoiceState} newState The voice state after the update + * @param {?VoiceState} oldState The voice state before the update + * @param {?VoiceState} newState The voice state after the update */ module.exports = VoiceStateUpdateHandler; diff --git a/typings/index.d.ts b/typings/index.d.ts index f431c3d19..23e015062 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -195,7 +195,7 @@ declare module 'discord.js' { public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; - public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; + public on(event: 'voiceStateUpdate', listener: (oldState?: VoiceState, newState?: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: string, listener: Function): this; @@ -227,7 +227,7 @@ declare module 'discord.js' { public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; - public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState?: VoiceState, newState?: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: string, listener: Function): this; } From 16076124b2687ebb9cde40ec8585fca4c6588869 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 14 Oct 2018 13:58:34 -0500 Subject: [PATCH 0913/1359] fix(Client#voiceStateUpdate): newState is guaranteed --- src/client/websocket/packets/handlers/VoiceStateUpdate.js | 7 +++---- typings/index.d.ts | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/websocket/packets/handlers/VoiceStateUpdate.js index d6cc08aee..e423d0d0b 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/websocket/packets/handlers/VoiceStateUpdate.js @@ -29,9 +29,8 @@ class VoiceStateUpdateHandler extends AbstractHandler { if (member && member.user.id === client.user.id && data.channel_id) { client.emit('self.voiceStateUpdate', data); } - if (oldState || newState) { - client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); - } + + client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); } } } @@ -40,7 +39,7 @@ class VoiceStateUpdateHandler extends AbstractHandler { * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate * @param {?VoiceState} oldState The voice state before the update - * @param {?VoiceState} newState The voice state after the update + * @param {VoiceState} newState The voice state after the update */ module.exports = VoiceStateUpdateHandler; diff --git a/typings/index.d.ts b/typings/index.d.ts index 23e015062..70ff34b39 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -195,7 +195,7 @@ declare module 'discord.js' { public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; - public on(event: 'voiceStateUpdate', listener: (oldState?: VoiceState, newState?: VoiceState) => void): this; + public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: string, listener: Function): this; @@ -227,7 +227,7 @@ declare module 'discord.js' { public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; - public once(event: 'voiceStateUpdate', listener: (oldState?: VoiceState, newState?: VoiceState) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: string, listener: Function): this; } From e189c1b728d8fd2742acc4d807e7559e807814fd Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Mon, 15 Oct 2018 02:08:49 -0500 Subject: [PATCH 0914/1359] fix: revert #2744 see #2848, fixes issues with stack not displaying correctly --- src/rest/APIRouter.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 678f2fe70..06ebeb3f9 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -11,13 +11,6 @@ function buildRoute(manager) { get(target, name) { if (reflectors.includes(name)) return () => route.join('/'); if (methods.includes(name)) { - // Preserve async stack - let stackTrace = null; - if (Error.captureStackTrace) { - stackTrace = {}; - Error.captureStackTrace(stackTrace, this.get); - } - return options => manager.request(name, route.join('/'), Object.assign({ versioned: manager.versioned, route: route.map((r, i) => { @@ -25,14 +18,7 @@ function buildRoute(manager) { if (route[i - 1] === 'reactions') return ':reaction'; return r; }).join('/'), - }, options)).catch(error => { - if (stackTrace && (error instanceof Error)) { - stackTrace.name = error.name; - stackTrace.message = error.message; - error.stack = stackTrace.stack; - } - throw error; - }); + }, options)); } route.push(name); return new Proxy(noop, handler); From 2ba00038d158ca40d59a9c83e2ac4f25e9d4baf0 Mon Sep 17 00:00:00 2001 From: sillyfrog <816454+sillyfrog@users.noreply.github.com> Date: Sat, 20 Oct 2018 02:15:01 +1000 Subject: [PATCH 0915/1359] fix(VoiceConnection): use Speaking#has to fire _stoppedSpeaking (#2896) * Use match on speaking bitmask to fire _stoppedSpeaking * Use constants and correct matching * Correctly do a not check --- src/client/voice/VoiceConnection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6e0557995..7dc06fa3d 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -447,12 +447,12 @@ class VoiceConnection extends EventEmitter { */ if (this.status === VoiceStatus.CONNECTED) { this.emit('speaking', user, speaking); - if (!speaking) { + if (!speaking.has(Speaking.FLAGS.SPEAKING)) { this.receiver.packets._stoppedSpeaking(user_id); } } - if (guild && user && old !== speaking) { + if (guild && user && !old.equals(speaking)) { const member = guild.member(user); if (member) { /** From 01476de58250761ffe5eedc9ee6c9782576ca043 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 20 Oct 2018 12:12:21 +0200 Subject: [PATCH 0916/1359] fix(VoiceConnection): compare new speaking to old to avoid TypeError --- src/client/voice/VoiceConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 7dc06fa3d..9bec81509 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -452,7 +452,7 @@ class VoiceConnection extends EventEmitter { } } - if (guild && user && !old.equals(speaking)) { + if (guild && user && !speaking.equals(old)) { const member = guild.member(user); if (member) { /** From b759fc415e89c1f3eb3523deb23c5b514968c845 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 27 Oct 2018 15:28:05 -0400 Subject: [PATCH 0917/1359] Slightly simplify logic in APIMessage.transformOptions --- src/structures/APIMessage.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index bb4d16179..8b714fcd5 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -287,13 +287,9 @@ class APIMessage { if (!options) { options = {}; - } - - if (options instanceof MessageEmbed) { + } else if (options instanceof MessageEmbed) { return isWebhook ? { content, embeds: [options], ...extra } : { content, embed: options, ...extra }; - } - - if (options instanceof MessageAttachment) { + } else if (options instanceof MessageAttachment) { return { content, files: [options], ...extra }; } From ab3a43919890f0a45812fd91788ea911a106f937 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 29 Oct 2018 15:02:36 -0400 Subject: [PATCH 0918/1359] Add worker-based sharding to the ShardingManager (#2908) * Add worker-based sharding mode to ShardingManager * Fix ClientShardUtil mode * Fix worker not being cleared on shard death * Update docs and typings * Clean up Client sharding logic a bit * Add info about requirements for worker mode --- src/client/Client.js | 34 ++++++++-- src/sharding/Shard.js | 113 ++++++++++++++++++++------------ src/sharding/ShardClientUtil.js | 74 +++++++++++++++------ src/sharding/ShardingManager.js | 40 +++++++++-- typings/index.d.ts | 11 +++- 5 files changed, 196 insertions(+), 76 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index b87d21c94..4b3160dfe 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -31,12 +31,30 @@ class Client extends BaseClient { constructor(options = {}) { super(Object.assign({ _tokenType: 'Bot' }, options)); - // Obtain shard details from environment - if (!browser && !this.options.shardId && 'SHARD_ID' in process.env) { - this.options.shardId = Number(process.env.SHARD_ID); - } - if (!browser && !this.options.shardCount && 'SHARD_COUNT' in process.env) { - this.options.shardCount = Number(process.env.SHARD_COUNT); + // Figure out the shard details + if (!browser && process.env.SHARDING_MANAGER) { + // Try loading workerData if it's present + let workerData; + try { + workerData = require('worker_threads').workerData; + } catch (err) { + // Do nothing + } + + if (!this.options.shardId) { + if (workerData && 'SHARD_ID' in workerData) { + this.options.shardId = workerData.SHARD_ID; + } else if ('SHARD_ID' in process.env) { + this.options.shardId = Number(process.env.SHARD_ID); + } + } + if (!this.options.shardCount) { + if (workerData && 'SHARD_COUNT' in workerData) { + this.options.shardCount = workerData.SHARD_COUNT; + } else if ('SHARD_COUNT' in process.env) { + this.options.shardCount = Number(process.env.SHARD_COUNT); + } + } } this._validateOptions(); @@ -73,7 +91,9 @@ class Client extends BaseClient { * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * @type {?ShardClientUtil} */ - this.shard = !browser && process.env.SHARDING_MANAGER ? ShardClientUtil.singleton(this) : null; + this.shard = !browser && process.env.SHARDING_MANAGER ? + ShardClientUtil.singleton(this, process.env.SHARDING_MANAGER_MODE) : + null; /** * All of the {@link User} objects that have been cached at any point, mapped by their IDs diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 1fb479752..a43bb7e92 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -1,13 +1,14 @@ -const childProcess = require('child_process'); const EventEmitter = require('events'); const path = require('path'); const Util = require('../util/Util'); const { Error } = require('../errors'); +let childProcess = null; +let Worker = null; /** * A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains - * an instance of the bot and its {@link Client}. When its child process exits for any reason, the shard will spawn a - * new one to replace it as necessary. + * an instance of the bot and its {@link Client}. When its child process/worker exits for any reason, the shard will + * spawn a new one to replace it as necessary. * @extends EventEmitter */ class Shard extends EventEmitter { @@ -18,6 +19,9 @@ class Shard extends EventEmitter { constructor(manager, id) { super(); + if (manager.mode === 'process') childProcess = require('child_process'); + else if (manager.mode === 'worker') Worker = require('worker_threads').Worker; + /** * Manager that created the shard * @type {ShardingManager} @@ -31,26 +35,24 @@ class Shard extends EventEmitter { this.id = id; /** - * Arguments for the shard's process + * Arguments for the shard's process (only when {@link ShardingManager#mode} is `process`) * @type {string[]} */ this.args = manager.shardArgs || []; /** - * Arguments for the shard's process executable + * Arguments for the shard's process executable (only when {@link ShardingManager#mode} is `process`) * @type {?string[]} */ this.execArgv = manager.execArgv; /** - * Environment variables for the shard's process + * Environment variables for the shard's process, or workerData for the shard's worker * @type {Object} */ this.env = Object.assign({}, process.env, { - SHARDING_MANAGER: true, SHARD_ID: this.id, SHARD_COUNT: this.manager.totalShards, - DISCORD_TOKEN: this.manager.token, }); /** @@ -60,11 +62,17 @@ class Shard extends EventEmitter { this.ready = false; /** - * Process of the shard + * Process of the shard (if {@link ShardingManager#mode} is `process`) * @type {?ChildProcess} */ this.process = null; + /** + * Worker of the shard (if {@link ShardingManager#mode} is `worker`) + * @type {?Worker} + */ + this.worker = null; + /** * Ongoing promises for calls to {@link Shard#eval}, mapped by the `script` they were called with * @type {Map} @@ -88,49 +96,62 @@ class Shard extends EventEmitter { } /** - * Forks a child process for the shard. + * Forks a child process or creates a worker thread for the shard. * You should not need to call this manually. * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving * @returns {Promise} */ async spawn(waitForReady = true) { if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id); + if (this.worker) throw new Error('SHARDING_WORKER_EXISTS', this.id); - this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { - env: this.env, execArgv: this.execArgv, - }) - .on('message', this._handleMessage.bind(this)) - .on('exit', this._exitListener); + if (this.manager.mode === 'process') { + this.process = childProcess.fork(path.resolve(this.manager.file), this.args, { + env: this.env, execArgv: this.execArgv, + }) + .on('message', this._handleMessage.bind(this)) + .on('exit', this._exitListener); + } else if (this.manager.mode === 'worker') { + this.worker = new Worker(path.resolve(this.manager.file), { workerData: this.env }) + .on('message', this._handleMessage.bind(this)) + .on('exit', this._exitListener); + } /** - * Emitted upon the creation of the shard's child process. + * Emitted upon the creation of the shard's child process/worker. * @event Shard#spawn - * @param {ChildProcess} process Child process that was created + * @param {ChildProcess|Worker} process Child process/worker that was created */ - this.emit('spawn', this.process); + this.emit('spawn', this.process || this.worker); - if (!waitForReady) return this.process; + if (!waitForReady) return this.process || this.worker; await new Promise((resolve, reject) => { this.once('ready', resolve); this.once('disconnect', () => reject(new Error('SHARDING_READY_DISCONNECTED', this.id))); this.once('death', () => reject(new Error('SHARDING_READY_DIED', this.id))); setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), 30000); }); - return this.process; + return this.process || this.worker; } /** - * Immediately kills the shard's process and does not restart it. + * Immediately kills the shard's process/worker and does not restart it. */ kill() { - this.process.removeListener('exit', this._exitListener); - this.process.kill(); + if (this.process) { + this.process.removeListener('exit', this._exitListener); + this.process.kill(); + } else { + this.worker.removeListener('exit', this._exitListener); + this.worker.terminate(); + } + this._handleExit(false); } /** - * Kills and restarts the shard's process. - * @param {number} [delay=500] How long to wait between killing the process and restarting it (in milliseconds) + * Kills and restarts the shard's process/worker. + * @param {number} [delay=500] How long to wait between killing the process/worker and restarting it (in milliseconds) * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving * @returns {Promise} */ @@ -141,15 +162,20 @@ class Shard extends EventEmitter { } /** - * Sends a message to the shard's process. + * Sends a message to the shard's process/worker. * @param {*} message Message to send to the shard * @returns {Promise} */ send(message) { return new Promise((resolve, reject) => { - this.process.send(message, err => { - if (err) reject(err); else resolve(this); - }); + if (this.process) { + this.process.send(message, err => { + if (err) reject(err); else resolve(this); + }); + } else { + this.worker.postMessage(message); + resolve(this); + } }); } @@ -166,16 +192,18 @@ class Shard extends EventEmitter { if (this._fetches.has(prop)) return this._fetches.get(prop); const promise = new Promise((resolve, reject) => { + const child = this.process || this.worker; + const listener = message => { if (!message || message._fetchProp !== prop) return; - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._fetches.delete(prop); resolve(message._result); }; - this.process.on('message', listener); + child.on('message', listener); this.send({ _fetchProp: prop }).catch(err => { - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._fetches.delete(prop); reject(err); }); @@ -194,17 +222,19 @@ class Shard extends EventEmitter { if (this._evals.has(script)) return this._evals.get(script); const promise = new Promise((resolve, reject) => { + const child = this.process || this.worker; + const listener = message => { if (!message || message._eval !== script) return; - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._evals.delete(script); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - this.process.on('message', listener); + child.on('message', listener); const _eval = typeof script === 'function' ? `(${script})(this)` : script; this.send({ _eval }).catch(err => { - this.process.removeListener('message', listener); + child.removeListener('message', listener); this._evals.delete(script); reject(err); }); @@ -215,7 +245,7 @@ class Shard extends EventEmitter { } /** - * Handles an IPC message. + * Handles a message received from the child process/worker. * @param {*} message Message received * @private */ @@ -283,7 +313,7 @@ class Shard extends EventEmitter { } /** - * Emitted upon recieving a message from the child process. + * Emitted upon recieving a message from the child process/worker. * @event Shard#message * @param {*} message Message that was received */ @@ -291,20 +321,21 @@ class Shard extends EventEmitter { } /** - * Handles the shard's process exiting. + * Handles the shard's process/worker exiting. * @param {boolean} [respawn=this.manager.respawn] Whether to spawn the shard again * @private */ _handleExit(respawn = this.manager.respawn) { /** - * Emitted upon the shard's child process exiting. + * Emitted upon the shard's child process/worker exiting. * @event Shard#death - * @param {ChildProcess} process Child process that exited + * @param {ChildProcess|Worker} process Child process/worker that exited */ - this.emit('death', this.process); + this.emit('death', this.process || this.worker); this.ready = false; this.process = null; + this.worker = null; this._evals.clear(); this._fetches.clear(); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 07c282ebf..4aa35a3b2 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -2,19 +2,45 @@ const Util = require('../util/Util'); const { Events } = require('../util/Constants'); /** - * Helper class for sharded clients spawned as a child process, such as from a {@link ShardingManager}. + * Helper class for sharded clients spawned as a child process/worker, such as from a {@link ShardingManager}. * Utilises IPC to send and receive data to/from the master process and other shards. */ class ShardClientUtil { /** * @param {Client} client Client of the current shard + * @param {ShardingManagerMode} mode Mode the shard was spawned with */ - constructor(client) { + constructor(client, mode) { + /** + * Client for the shard + * @type {Client} + */ this.client = client; - process.on('message', this._handleMessage.bind(this)); - client.on('ready', () => { process.send({ _ready: true }); }); - client.on('disconnect', () => { process.send({ _disconnect: true }); }); - client.on('reconnecting', () => { process.send({ _reconnecting: true }); }); + + /** + * Mode the shard was spawned with + * @type {ShardingManagerMode} + */ + this.mode = mode; + + /** + * Message port for the master process (only when {@link ShardClientUtil#mode} is `worker`) + * @type {?MessagePort} + */ + this.parentPort = null; + + if (mode === 'process') { + process.on('message', this._handleMessage.bind(this)); + client.on('ready', () => { process.send({ _ready: true }); }); + client.on('disconnect', () => { process.send({ _disconnect: true }); }); + client.on('reconnecting', () => { process.send({ _reconnecting: true }); }); + } else if (mode === 'worker') { + this.parentPort = require('worker_threads').parentPort; + this.parentPort.on('message', this._handleMessage.bind(this)); + client.on('ready', () => { this.parentPort.postMessage({ _ready: true }); }); + client.on('disconnect', () => { this.parentPort.postMessage({ _disconnect: true }); }); + client.on('reconnecting', () => { this.parentPort.postMessage({ _reconnecting: true }); }); + } } /** @@ -42,9 +68,14 @@ class ShardClientUtil { */ send(message) { return new Promise((resolve, reject) => { - process.send(message, err => { - if (err) reject(err); else resolve(); - }); + if (this.mode === 'process') { + process.send(message, err => { + if (err) reject(err); else resolve(); + }); + } else if (this.mode === 'worker') { + this.parentPort.postMessage(message); + resolve(); + } }); } @@ -60,15 +91,17 @@ class ShardClientUtil { */ fetchClientValues(prop) { return new Promise((resolve, reject) => { + const parent = this.parentPort || process; + const listener = message => { if (!message || message._sFetchProp !== prop) return; - process.removeListener('message', listener); + parent.removeListener('message', listener); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - process.on('message', listener); + parent.on('message', listener); this.send({ _sFetchProp: prop }).catch(err => { - process.removeListener('message', listener); + parent.removeListener('message', listener); reject(err); }); }); @@ -86,16 +119,18 @@ class ShardClientUtil { */ broadcastEval(script) { return new Promise((resolve, reject) => { + const parent = this.parentPort || process; script = typeof script === 'function' ? `(${script})(this)` : script; + const listener = message => { if (!message || message._sEval !== script) return; - process.removeListener('message', listener); + parent.removeListener('message', listener); if (!message._error) resolve(message._result); else reject(Util.makeError(message._error)); }; - process.on('message', listener); + parent.on('message', listener); this.send({ _sEval: script }).catch(err => { - process.removeListener('message', listener); + parent.removeListener('message', listener); reject(err); }); }); @@ -104,7 +139,7 @@ class ShardClientUtil { /** * Requests a respawn of all shards. * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds) - * @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it + * @param {number} [respawnDelay=500] How long to wait between killing a shard's process/worker and restarting it * (in milliseconds) * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another * @returns {Promise} Resolves upon the message being sent @@ -151,14 +186,15 @@ class ShardClientUtil { /** * Creates/gets the singleton of this class. * @param {Client} client The client to use + * @param {ShardingManagerMode} mode Mode the shard was spawned with * @returns {ShardClientUtil} */ - static singleton(client) { + static singleton(client, mode) { if (!this._singleton) { - this._singleton = new this(client); + this._singleton = new this(client, mode); } else { client.emit(Events.WARN, - 'Multiple clients created in child process; only the first will handle sharding helpers.'); + 'Multiple clients created in child process/worker; only the first will handle sharding helpers.'); } return this._singleton; } diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index bbec15b67..7cedc37ef 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -8,29 +8,42 @@ const { Error, TypeError, RangeError } = require('../errors'); /** * This is a utility class that makes multi-process sharding of a bot an easy and painless experience. - * It works by spawning a self-contained {@link ChildProcess} for each individual shard, each containing its own - * instance of your bot's {@link Client}. They all have a line of communication with the master process, and there are - * several useful methods that utilise it in order to simplify tasks that are normally difficult with sharding. It can - * spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a path to your main bot - * script to launch for each one. + * It works by spawning a self-contained {@link ChildProcess} or {@link Worker} for each individual shard, each + * containing its own instance of your bot's {@link Client}. They all have a line of communication with the master + * process, and there are several useful methods that utilise it in order to simplify tasks that are normally difficult + * with sharding. It can spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a + * path to your main bot script to launch for each one. * @extends {EventEmitter} */ class ShardingManager extends EventEmitter { + /** + * The mode to spawn shards with for a {@link ShardingManager}: either "process" to use child processes, or + * "worker" to use workers. The "worker" mode relies on the experimental + * [Worker threads](https://nodejs.org/api/worker_threads.html) functionality that is present in Node v10.5.0 or + * newer. Node must be started with the `--experimental-worker` flag to expose it. + * @typedef {Object} ShardingManagerMode + */ + /** * @param {string} file Path to your shard script file * @param {Object} [options] Options for the sharding manager * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto" + * @param {ShardingManagerMode} [options.mode='process'] Which mode to use for shards * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting * @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning + * (only available when using the `process` mode) * @param {string[]} [options.execArgv=[]] Arguments to pass to the shard script executable when spawning + * (only available when using the `process` mode) * @param {string} [options.token] Token to use for automatic shard count and passing to shards */ constructor(file, options = {}) { super(); options = Util.mergeDefault({ totalShards: 'auto', + mode: 'process', respawn: true, shardArgs: [], + execArgv: [], token: process.env.DISCORD_TOKEN, }, options); @@ -59,6 +72,15 @@ class ShardingManager extends EventEmitter { } } + /** + * Mode for shards to spawn with + * @type {ShardingManagerMode} + */ + this.mode = options.mode; + if (this.mode !== 'process' && this.mode !== 'worker') { + throw new RangeError('CLIENT_INVALID_OPTION', 'Sharding mode', '"process" or "worker"'); + } + /** * Whether shards should automatically respawn upon exiting * @type {boolean} @@ -66,13 +88,13 @@ class ShardingManager extends EventEmitter { this.respawn = options.respawn; /** - * An array of arguments to pass to shards + * An array of arguments to pass to shards (only when {@link ShardingManager#mode} is `process`) * @type {string[]} */ this.shardArgs = options.shardArgs; /** - * An array of arguments to pass to the executable + * An array of arguments to pass to the executable (only when {@link ShardingManager#mode} is `process`) * @type {string[]} */ this.execArgv = options.execArgv; @@ -88,6 +110,10 @@ class ShardingManager extends EventEmitter { * @type {Collection} */ this.shards = new Collection(); + + process.env.SHARDING_MANAGER = true; + process.env.SHARDING_MANAGER_MODE = this.mode; + process.env.DISCORD_TOKEN = this.token; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 70ff34b39..30fe364a9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -921,6 +921,7 @@ declare module 'discord.js' { public manager: ShardingManager; public process: ChildProcess; public ready: boolean; + public worker: Worker; public eval(script: string): Promise; public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; @@ -945,24 +946,28 @@ declare module 'discord.js' { } export class ShardClientUtil { - constructor(client: Client); + constructor(client: Client, mode: ShardingManagerMode); private _handleMessage(message: any): void; private _respond(type: string, message: any): void; + public client: Client; public readonly count: number; public readonly id: number; + public mode: ShardingManagerMode; + public parentPort: MessagePort; public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise; public send(message: any): Promise; - public static singleton(client: Client): ShardClientUtil; + public static singleton(client: Client, mode: ShardingManagerMode): ShardClientUtil; } export class ShardingManager extends EventEmitter { constructor(file: string, options?: { totalShards?: number | 'auto'; + mode?: ShardingManagerMode; respawn?: boolean; shardArgs?: string[]; token?: string; @@ -2022,6 +2027,8 @@ declare module 'discord.js' { type RoleResolvable = Role | string; + type ShardingManagerMode = 'process' | 'worker'; + type Snowflake = string; type SplitOptions = { From 18f065867ce7f8369e2c660b62b093e092433822 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 3 Nov 2018 13:09:56 -0400 Subject: [PATCH 0919/1359] Fix #2924 with a bandage (Node typings not updated) --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 30fe364a9..2672d1845 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -921,7 +921,7 @@ declare module 'discord.js' { public manager: ShardingManager; public process: ChildProcess; public ready: boolean; - public worker: Worker; + public worker: any; public eval(script: string): Promise; public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; @@ -954,7 +954,7 @@ declare module 'discord.js' { public readonly count: number; public readonly id: number; public mode: ShardingManagerMode; - public parentPort: MessagePort; + public parentPort: any; public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; From f3cad81f5314a3997ac188277e2471ba84cbd771 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sat, 3 Nov 2018 13:21:23 -0500 Subject: [PATCH 0920/1359] feat: Internal sharding (#2902) * internal sharding * ready event * the square deal * the new deal * the second new deal * add actual documentation * the new freedom * the great society * federal intervention * some of requested changes * i ran out of things to call these * destroy this * fix: Client#uptime went missing * fix(Client): destroy the client on login failure This may happen duo invalid sharding config / invalid token / user requested destroy * fix(Client): reject login promise when the client is destroyed before ready * fix(WebSocketManager): remove redundancy in destroy method (#2491) * typo(ErrorMessages): duo -> duo to * typo(ErrorMessages): duo -> due * fix: docs and options * docs(WebSocketManager): WebSockethard -> WebSocketShard (#2502) * fix(ClientUser): lazily load to account for extended user structure (#2501) * docs(WebSocketShard): document class to make it visible in documentation (#2504) * fix: WebSocketShard#reconnect * fix: presenceUpdate & userUpdate * presenceUpdate wasn't really being handled at all * userUpdate handled incorrectly because as of v7 in the Discord API, it comes inside presenceUpdate * re-add raw event * member is now part of message create payload * feat: Add functionality to support multiple servers with different shards (#2395) * Added functionallity to spawn multiple sharding managers due to adding start and end shards * Small fixes and limiting shard amount to max recommended * Forgot a check in spawn() * Fixed indentation * Removed optiosn object documentation for totalShards * More fixes and a check that the startShard + amount doesnt go over the recommended shard amount * fix getting max recommended * Removed async from constructor (my fault) * Changed start and end shard to a shardList or "auto" + fixed some brainfarts with isNaN * Changed the loop and totalShard count calculation * shards are actually 0 based * Fixed a problem with the gateway and handled some range errors and type errors * Changed Number.isNan to isNaN and changed a few Integer checks to use Number.isInteger * Added check if shardList contains smth greater than totalShards; made spawn use totalShards again; shardList will be ignored and rebuild if totalShards is 'auto'; fixed docs * ShardingManager#spawn now uses a for..of loop; fixed the if statement inside the new for..of loop to still work as intended; made the totalShards be set to a new amount if smth manual is put into ShardingManager#spawn just like before; Fixed some spelling * internal sharding * ready event * the square deal * the new deal * the second new deal * add actual documentation * the new freedom * the great society * federal intervention * some of requested changes * i ran out of things to call these * destroy this * fix: Client#uptime went missing * fix(Client): destroy the client on login failure This may happen duo invalid sharding config / invalid token / user requested destroy * fix(Client): reject login promise when the client is destroyed before ready * fix(WebSocketManager): remove redundancy in destroy method (#2491) * typo(ErrorMessages): duo -> duo to * typo(ErrorMessages): duo -> due * fix: docs and options * docs(WebSocketManager): WebSockethard -> WebSocketShard (#2502) * fix(ClientUser): lazily load to account for extended user structure (#2501) * docs(WebSocketShard): document class to make it visible in documentation (#2504) * fix: WebSocketShard#reconnect * fix: presenceUpdate & userUpdate * presenceUpdate wasn't really being handled at all * userUpdate handled incorrectly because as of v7 in the Discord API, it comes inside presenceUpdate * Internal Sharding adaptation Adapted to internal sharding Fixed a bug where non ready invalidated sessions wouldnt respawn * Fixed shardCount not retrieving * Fixing style removed unnecessary parenthesis * Fixing and rebasing lets hope i didnt dun hecklered it * Fixing my own retardation * Thanks git rebase * fix: assigning member in message create payload * fix: resumes * fix: IS wont give up reconnecting now * docs: add missing docs mostly * fix: found lost methods * fix: WebSocketManager#broadcast check if shard exists * fix: ShardClientUtil#id returning undefined * feat: handle new session rate limits (#2796) * feat: handle new session rate limits * i have no idea what i was doing last night * fix if statement weirdness * fix: re-add presence parsing from ClientOptions (#2893) * resolve conflicts * typings: missing typings * re-add missing linter rule * fix: replacing ClientUser wrongly * address unecessary performance waste * docs: missing disconnect event * fix(typings): Fix 2 issues with typings (#2909) * (Typings) Update typings to reflect current ClientOptions * fix(Typings) fixes a bug with Websockets and DOM Types * fix travis * feat: allow setting presence per shard * add WebSocketManager#shardX events * adjust typings, docs and performance issues * readjust shard events, now provide shardId parameter instead * fix: ready event should check shardCount, not actualShardCount * fix: re-add replayed parameter of Client#resume * fix(Sharding): fixes several things in Internal Sharding (#2914) * fix(Sharding) fixes several things in Internal Sharding * add default value for shards property * better implement checking for shards array * fix travis & some casing * split shard count into 2 words * update to latest Internal Sharding, fix requested changes * make sure totalShardCount is a number * fix comment * fix small typo * dynamically set totalShardCount if either shards or shardCount is provided * consistency: rename shardID to shardId * remove Client#shardIds * fix: typo in GuildIntegrationsUpdate handler * fix: incorrect packet data being passed in some events (#2919) * fix: edgecase of ShardingManager and totalShardCount (#2918) * fix: Client#userUpdate being passed wrong parameter and fix a potential edgecase of returning null in ClientUser#edit from this event * fix consistency and typings issues * consistency: shardId instances renamed to shardID * typings: fix typings regarding WebSocket * style(.eslintrc): remove additional whitespace * fix(Client): remove ondisconnect handler on timeout * docs(BaseClient): fix typo of Immediate * nitpick: typings, private fields and methods * typo: improve grammar a bit * fix: error assigning client in WebSocketManager * typo: actually spell milliseconds properly --- package.json | 1 + src/client/BaseClient.js | 35 +- src/client/Client.js | 192 +++--- src/client/ClientManager.js | 70 --- src/client/actions/ActionsManager.js | 4 + src/client/actions/GuildIntegrationsUpdate.js | 18 + src/client/actions/GuildMemberRemove.js | 4 +- src/client/actions/MessageCreate.js | 4 +- src/client/actions/PresenceUpdate.js | 38 ++ src/client/actions/UserUpdate.js | 15 +- .../handlers => actions}/VoiceStateUpdate.js | 17 +- .../handlers => actions}/WebhooksUpdate.js | 11 +- src/client/voice/VoiceConnection.js | 2 +- src/client/websocket/WebSocketManager.js | 279 +++++++-- ...bSocketConnection.js => WebSocketShard.js} | 583 +++++++++--------- .../websocket/handlers/CHANNEL_CREATE.js | 3 + .../websocket/handlers/CHANNEL_DELETE.js | 3 + .../websocket/handlers/CHANNEL_PINS_UPDATE.js | 20 + .../websocket/handlers/CHANNEL_UPDATE.js | 15 + .../websocket/handlers/GUILD_BAN_ADD.js | 14 + .../websocket/handlers/GUILD_BAN_REMOVE.js | 3 + src/client/websocket/handlers/GUILD_CREATE.js | 26 + src/client/websocket/handlers/GUILD_DELETE.js | 3 + .../websocket/handlers/GUILD_EMOJIS_UPDATE.js | 3 + .../handlers/GUILD_INTEGRATIONS_UPDATE.js | 3 + .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 17 + .../websocket/handlers/GUILD_MEMBER_ADD.js | 17 + .../websocket/handlers/GUILD_MEMBER_REMOVE.js | 3 + .../websocket/handlers/GUILD_MEMBER_UPDATE.js | 20 + .../websocket/handlers/GUILD_ROLE_CREATE.js | 3 + .../websocket/handlers/GUILD_ROLE_DELETE.js | 3 + .../websocket/handlers/GUILD_ROLE_UPDATE.js | 3 + src/client/websocket/handlers/GUILD_SYNC.js | 3 + src/client/websocket/handlers/GUILD_UPDATE.js | 3 + .../websocket/handlers/MESSAGE_CREATE.js | 3 + .../websocket/handlers/MESSAGE_DELETE.js | 3 + .../websocket/handlers/MESSAGE_DELETE_BULK.js | 3 + .../handlers/MESSAGE_REACTION_ADD.js | 6 + .../handlers/MESSAGE_REACTION_REMOVE.js | 3 + .../handlers/MESSAGE_REACTION_REMOVE_ALL.js | 3 + .../websocket/handlers/MESSAGE_UPDATE.js | 14 + .../websocket/handlers/PRESENCE_UPDATE.js | 3 + src/client/websocket/handlers/READY.js | 16 + src/client/websocket/handlers/RESUMED.js | 12 + src/client/websocket/handlers/TYPING_START.js | 16 + src/client/websocket/handlers/USER_UPDATE.js | 3 + .../websocket/handlers/VOICE_SERVER_UPDATE.js | 3 + .../websocket/handlers/VOICE_STATE_UPDATE.js | 3 + .../websocket/handlers/WEBHOOKS_UPDATE.js | 3 + src/client/websocket/handlers/index.js | 11 + .../packets/WebSocketPacketManager.js | 104 ---- .../packets/handlers/AbstractHandler.js | 11 - .../packets/handlers/ChannelCreate.js | 15 - .../packets/handlers/ChannelDelete.js | 9 - .../packets/handlers/ChannelPinsUpdate.js | 37 -- .../packets/handlers/ChannelUpdate.js | 20 - .../websocket/packets/handlers/GuildBanAdd.js | 23 - .../packets/handlers/GuildBanRemove.js | 20 - .../websocket/packets/handlers/GuildCreate.js | 33 - .../websocket/packets/handlers/GuildDelete.js | 16 - .../packets/handlers/GuildEmojisUpdate.js | 11 - .../handlers/GuildIntegrationsUpdate.js | 19 - .../packets/handlers/GuildMemberAdd.js | 27 - .../packets/handlers/GuildMemberRemove.js | 13 - .../packets/handlers/GuildMemberUpdate.js | 29 - .../packets/handlers/GuildMembersChunk.js | 28 - .../packets/handlers/GuildRoleCreate.js | 11 - .../packets/handlers/GuildRoleDelete.js | 11 - .../packets/handlers/GuildRoleUpdate.js | 11 - .../websocket/packets/handlers/GuildUpdate.js | 11 - .../packets/handlers/MessageCreate.js | 9 - .../packets/handlers/MessageDelete.js | 9 - .../packets/handlers/MessageDeleteBulk.js | 9 - .../packets/handlers/MessageReactionAdd.js | 13 - .../packets/handlers/MessageReactionRemove.js | 11 - .../handlers/MessageReactionRemoveAll.js | 11 - .../packets/handlers/MessageUpdate.js | 20 - .../packets/handlers/PresenceUpdate.js | 68 -- .../websocket/packets/handlers/Ready.js | 41 -- .../websocket/packets/handlers/Resumed.js | 28 - .../websocket/packets/handlers/TypingStart.js | 68 -- .../websocket/packets/handlers/UserUpdate.js | 11 - .../packets/handlers/VoiceServerUpdate.js | 19 - src/errors/Messages.js | 1 + src/sharding/Shard.js | 8 +- src/sharding/ShardClientUtil.js | 2 +- src/sharding/ShardingManager.js | 48 +- src/stores/GuildMemberStore.js | 2 +- src/structures/ClientPresence.js | 10 +- src/structures/ClientUser.js | 19 +- src/structures/Guild.js | 15 + src/util/Constants.js | 23 +- test/shard.js | 6 +- test/tester1000.js | 4 +- typings/index.d.ts | 81 ++- 95 files changed, 1145 insertions(+), 1393 deletions(-) delete mode 100644 src/client/ClientManager.js create mode 100644 src/client/actions/GuildIntegrationsUpdate.js create mode 100644 src/client/actions/PresenceUpdate.js rename src/client/{websocket/packets/handlers => actions}/VoiceStateUpdate.js (75%) rename src/client/{websocket/packets/handlers => actions}/WebhooksUpdate.js (57%) rename src/client/websocket/{WebSocketConnection.js => WebSocketShard.js} (52%) create mode 100644 src/client/websocket/handlers/CHANNEL_CREATE.js create mode 100644 src/client/websocket/handlers/CHANNEL_DELETE.js create mode 100644 src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js create mode 100644 src/client/websocket/handlers/CHANNEL_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_BAN_ADD.js create mode 100644 src/client/websocket/handlers/GUILD_BAN_REMOVE.js create mode 100644 src/client/websocket/handlers/GUILD_CREATE.js create mode 100644 src/client/websocket/handlers/GUILD_DELETE.js create mode 100644 src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBER_ADD.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js create mode 100644 src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_ROLE_CREATE.js create mode 100644 src/client/websocket/handlers/GUILD_ROLE_DELETE.js create mode 100644 src/client/websocket/handlers/GUILD_ROLE_UPDATE.js create mode 100644 src/client/websocket/handlers/GUILD_SYNC.js create mode 100644 src/client/websocket/handlers/GUILD_UPDATE.js create mode 100644 src/client/websocket/handlers/MESSAGE_CREATE.js create mode 100644 src/client/websocket/handlers/MESSAGE_DELETE.js create mode 100644 src/client/websocket/handlers/MESSAGE_DELETE_BULK.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_ADD.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js create mode 100644 src/client/websocket/handlers/MESSAGE_UPDATE.js create mode 100644 src/client/websocket/handlers/PRESENCE_UPDATE.js create mode 100644 src/client/websocket/handlers/READY.js create mode 100644 src/client/websocket/handlers/RESUMED.js create mode 100644 src/client/websocket/handlers/TYPING_START.js create mode 100644 src/client/websocket/handlers/USER_UPDATE.js create mode 100644 src/client/websocket/handlers/VOICE_SERVER_UPDATE.js create mode 100644 src/client/websocket/handlers/VOICE_STATE_UPDATE.js create mode 100644 src/client/websocket/handlers/WEBHOOKS_UPDATE.js create mode 100644 src/client/websocket/handlers/index.js delete mode 100644 src/client/websocket/packets/WebSocketPacketManager.js delete mode 100644 src/client/websocket/packets/handlers/AbstractHandler.js delete mode 100644 src/client/websocket/packets/handlers/ChannelCreate.js delete mode 100644 src/client/websocket/packets/handlers/ChannelDelete.js delete mode 100644 src/client/websocket/packets/handlers/ChannelPinsUpdate.js delete mode 100644 src/client/websocket/packets/handlers/ChannelUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildBanAdd.js delete mode 100644 src/client/websocket/packets/handlers/GuildBanRemove.js delete mode 100644 src/client/websocket/packets/handlers/GuildCreate.js delete mode 100644 src/client/websocket/packets/handlers/GuildDelete.js delete mode 100644 src/client/websocket/packets/handlers/GuildEmojisUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildMemberAdd.js delete mode 100644 src/client/websocket/packets/handlers/GuildMemberRemove.js delete mode 100644 src/client/websocket/packets/handlers/GuildMemberUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildMembersChunk.js delete mode 100644 src/client/websocket/packets/handlers/GuildRoleCreate.js delete mode 100644 src/client/websocket/packets/handlers/GuildRoleDelete.js delete mode 100644 src/client/websocket/packets/handlers/GuildRoleUpdate.js delete mode 100644 src/client/websocket/packets/handlers/GuildUpdate.js delete mode 100644 src/client/websocket/packets/handlers/MessageCreate.js delete mode 100644 src/client/websocket/packets/handlers/MessageDelete.js delete mode 100644 src/client/websocket/packets/handlers/MessageDeleteBulk.js delete mode 100644 src/client/websocket/packets/handlers/MessageReactionAdd.js delete mode 100644 src/client/websocket/packets/handlers/MessageReactionRemove.js delete mode 100644 src/client/websocket/packets/handlers/MessageReactionRemoveAll.js delete mode 100644 src/client/websocket/packets/handlers/MessageUpdate.js delete mode 100644 src/client/websocket/packets/handlers/PresenceUpdate.js delete mode 100644 src/client/websocket/packets/handlers/Ready.js delete mode 100644 src/client/websocket/packets/handlers/Resumed.js delete mode 100644 src/client/websocket/packets/handlers/TypingStart.js delete mode 100644 src/client/websocket/packets/handlers/UserUpdate.js delete mode 100644 src/client/websocket/packets/handlers/VoiceServerUpdate.js diff --git a/package.json b/package.json index 4f27bf482..c3b4b40c4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node-fetch": "^2.1.2", "pako": "^1.0.0", "prism-media": "amishshah/prism-media", + "setimmediate": "^1.0.5", "tweetnacl": "^1.0.0", "ws": "^6.0.0" }, diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index 6e671f99e..c9fce5908 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -1,3 +1,4 @@ +require('setimmediate'); const EventEmitter = require('events'); const RESTManager = require('../rest/RESTManager'); const Util = require('../util/Util'); @@ -25,6 +26,13 @@ class BaseClient extends EventEmitter { */ this._intervals = new Set(); + /** + * Intervals set by {@link BaseClient#setImmediate} that are still active + * @type {Set} + * @private + */ + this._immediates = new Set(); + /** * The options the client was instantiated with * @type {ClientOptions} @@ -53,10 +61,12 @@ class BaseClient extends EventEmitter { * Destroys all assets used by the base client. */ destroy() { - for (const t of this._timeouts) clearTimeout(t); - for (const i of this._intervals) clearInterval(i); + for (const t of this._timeouts) this.clearTimeout(t); + for (const i of this._intervals) this.clearInterval(i); + for (const i of this._immediates) this.clearImmediate(i); this._timeouts.clear(); this._intervals.clear(); + this._immediates.clear(); } /** @@ -106,6 +116,27 @@ class BaseClient extends EventEmitter { this._intervals.delete(interval); } + /** + * Sets an immediate that will be automatically cancelled if the client is destroyed. + * @param {Function} fn Function to execute + * @param {...*} args Arguments for the function + * @returns {Immediate} + */ + setImmediate(fn, ...args) { + const immediate = setImmediate(fn, ...args); + this._immediates.add(immediate); + return immediate; + } + + /** + * Clears an immediate. + * @param {Immediate} immediate Immediate to cancel + */ + clearImmediate(immediate) { + clearImmediate(immediate); + this._immediates.delete(immediate); + } + toJSON(...props) { return Util.flatten(this, { domain: false }, ...props); } diff --git a/src/client/Client.js b/src/client/Client.js index 4b3160dfe..339e55646 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,6 +1,5 @@ const BaseClient = require('./BaseClient'); const Permissions = require('../util/Permissions'); -const ClientManager = require('./ClientManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); const WebSocketManager = require('./websocket/WebSocketManager'); const ActionsManager = require('./actions/ActionsManager'); @@ -15,7 +14,8 @@ const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); -const { Events, browser } = require('../util/Constants'); +const { Events, WSCodes, browser, DefaultOptions } = require('../util/Constants'); +const { delayFor } = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Structures = require('../util/Structures'); const { Error, TypeError, RangeError } = require('../errors'); @@ -31,45 +31,34 @@ class Client extends BaseClient { constructor(options = {}) { super(Object.assign({ _tokenType: 'Bot' }, options)); - // Figure out the shard details - if (!browser && process.env.SHARDING_MANAGER) { - // Try loading workerData if it's present - let workerData; - try { - workerData = require('worker_threads').workerData; - } catch (err) { - // Do nothing + // Obtain shard details from environment or if present, worker threads + let data = process.env; + try { + // Test if worker threads module is present and used + data = require('worker_threads').workerData || data; + } catch (_) { + // Do nothing + } + if (this.options.shards === DefaultOptions.shards) { + if ('SHARDS' in data) { + this.options.shards = JSON.parse(data.SHARDS); } - - if (!this.options.shardId) { - if (workerData && 'SHARD_ID' in workerData) { - this.options.shardId = workerData.SHARD_ID; - } else if ('SHARD_ID' in process.env) { - this.options.shardId = Number(process.env.SHARD_ID); - } - } - if (!this.options.shardCount) { - if (workerData && 'SHARD_COUNT' in workerData) { - this.options.shardCount = workerData.SHARD_COUNT; - } else if ('SHARD_COUNT' in process.env) { - this.options.shardCount = Number(process.env.SHARD_COUNT); - } + } + if (this.options.totalShardCount === DefaultOptions.totalShardCount) { + if ('TOTAL_SHARD_COUNT' in data) { + this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); + } else if (Array.isArray(this.options.shards)) { + this.options.totalShardCount = this.options.shards.length; + } else { + this.options.totalShardCount = this.options.shardCount; } } this._validateOptions(); - /** - * The manager of the client - * @type {ClientManager} - * @private - */ - this.manager = new ClientManager(this); - /** * The WebSocket manager of the client * @type {WebSocketManager} - * @private */ this.ws = new WebSocketManager(this); @@ -155,54 +144,11 @@ class Client extends BaseClient { */ this.broadcasts = []; - /** - * Previous heartbeat pings of the websocket (most recent first, limited to three elements) - * @type {number[]} - */ - this.pings = []; - if (this.options.messageSweepInterval > 0) { this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); } } - /** - * Timestamp of the latest ping's start time - * @type {number} - * @readonly - * @private - */ - get _pingTimestamp() { - return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0; - } - - /** - * Current status of the client's connection to Discord - * @type {?Status} - * @readonly - */ - get status() { - return this.ws.connection ? this.ws.connection.status : null; - } - - /** - * How long it has been since the client last entered the `READY` state in milliseconds - * @type {?number} - * @readonly - */ - get uptime() { - return this.readyAt ? Date.now() - this.readyAt : null; - } - - /** - * Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property - * @type {number} - * @readonly - */ - get ping() { - return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; - } - /** * All active voice connections that have been established, mapped by guild ID * @type {Collection} @@ -235,6 +181,15 @@ class Client extends BaseClient { return this.readyAt ? this.readyAt.getTime() : null; } + /** + * How long it has been since the client last entered the `READY` state in milliseconds + * @type {?number} + * @readonly + */ + get uptime() { + return this.readyAt ? Date.now() - this.readyAt : null; + } + /** * Creates a voice broadcast. * @returns {VoiceBroadcast} @@ -252,15 +207,54 @@ class Client extends BaseClient { * @example * client.login('my token'); */ - login(token = this.token) { - return new Promise((resolve, reject) => { - if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); - token = token.replace(/^Bot\s*/i, ''); - this.manager.connectToWebSocket(token, resolve, reject); - }).catch(e => { - this.destroy(); - return Promise.reject(e); + async login(token = this.token) { + if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); + this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); + this.emit(Events.DEBUG, `Authenticating using token ${token}`); + let endpoint = this.api.gateway; + if (this.options.shardCount === 'auto') endpoint = endpoint.bot; + const res = await endpoint.get(); + if (this.options.presence) { + this.options.ws.presence = await this.presence._parse(this.options.presence); + } + if (res.session_start_limit && res.session_start_limit.remaining === 0) { + const { session_start_limit: { reset_after } } = res; + this.emit(Events.DEBUG, `Exceeded identify threshold, setting a timeout for ${reset_after} ms`); + await delayFor(reset_after); + } + const gateway = `${res.url}/`; + if (this.options.shardCount === 'auto') { + this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); + this.options.shardCount = res.shards; + this.options.totalShardCount = res.shards; + } + this.emit(Events.DEBUG, `Using gateway ${gateway}`); + this.ws.connect(gateway); + await new Promise((resolve, reject) => { + const onready = () => { + clearTimeout(timeout); + this.removeListener(Events.DISCONNECT, ondisconnect); + resolve(); + }; + const ondisconnect = event => { + clearTimeout(timeout); + this.removeListener(Events.READY, onready); + this.destroy(); + if (WSCodes[event.code]) { + reject(new Error(WSCodes[event.code])); + } + }; + const timeout = setTimeout(() => { + this.removeListener(Events.READY, onready); + this.removeListener(Events.DISCONNECT, ondisconnect); + this.destroy(); + reject(new Error('WS_CONNECTION_TIMEOUT')); + }, this.options.shardCount * 25e3); + if (timeout.unref !== undefined) timeout.unref(); + this.once(Events.READY, onready); + this.once(Events.DISCONNECT, ondisconnect); }); + return token; } /** @@ -269,7 +263,8 @@ class Client extends BaseClient { */ destroy() { super.destroy(); - return this.manager.destroy(); + this.ws.destroy(); + this.token = null; } /** @@ -386,22 +381,10 @@ class Client extends BaseClient { return super.toJSON({ readyAt: false, broadcasts: false, - pings: false, presences: false, }); } - /** - * Adds a ping to {@link Client#pings}. - * @param {number} startTime Starting time of the ping - * @private - */ - _pong(startTime) { - this.pings.unshift(Date.now() - startTime); - if (this.pings.length > 3) this.pings.length = 3; - this.ws.lastHeartbeatAck = true; - } - /** * Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script * with the client as `this`. @@ -419,17 +402,13 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { // eslint-disable-line complexity - if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number'); + if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); } - if (typeof options.shardId !== 'number' || isNaN(options.shardId)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardId', 'a number'); - } - if (options.shardCount < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 0'); - if (options.shardId < 0) throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'at least 0'); - if (options.shardId !== 0 && options.shardId >= options.shardCount) { - throw new RangeError('CLIENT_INVALID_OPTION', 'shardId', 'less than shardCount'); + if (options.shards && typeof options.shards !== 'number' && !Array.isArray(options.shards)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); } + if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1'); if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); } @@ -451,9 +430,6 @@ class Client extends BaseClient { if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); } - if (typeof options.internalSharding !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'internalSharding', 'a boolean'); - } if (!(options.disabledEvents instanceof Array)) { throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array'); } diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js deleted file mode 100644 index e6d107822..000000000 --- a/src/client/ClientManager.js +++ /dev/null @@ -1,70 +0,0 @@ -const { Events, Status } = require('../util/Constants'); -const { Error } = require('../errors'); - -/** - * Manages the state and background tasks of the client. - * @private - */ -class ClientManager { - constructor(client) { - /** - * The client that instantiated this Manager - * @type {Client} - */ - this.client = client; - - /** - * The heartbeat interval - * @type {?number} - */ - this.heartbeatInterval = null; - } - - /** - * The status of the client - * @readonly - * @type {number} - */ - get status() { - return this.connection ? this.connection.status : Status.IDLE; - } - - /** - * Connects the client to the WebSocket. - * @param {string} token The authorization token - * @param {Function} resolve Function to run when connection is successful - * @param {Function} reject Function to run when connection fails - */ - connectToWebSocket(token, resolve, reject) { - this.client.emit(Events.DEBUG, `Authenticated using token ${token}`); - this.client.token = token; - const timeout = this.client.setTimeout(() => reject(new Error('WS_CONNECTION_TIMEOUT')), 1000 * 300); - this.client.api.gateway.get().then(async res => { - if (this.client.options.presence != null) { // eslint-disable-line eqeqeq - const presence = await this.client.presence._parse(this.client.options.presence); - this.client.options.ws.presence = presence; - this.client.presence.patch(presence); - } - const gateway = `${res.url}/`; - this.client.emit(Events.DEBUG, `Using gateway ${gateway}`); - this.client.ws.connect(gateway); - this.client.ws.connection.once('error', reject); - this.client.ws.connection.once('close', event => { - if (event.code === 4004) reject(new Error('TOKEN_INVALID')); - if (event.code === 4010) reject(new Error('SHARDING_INVALID')); - if (event.code === 4011) reject(new Error('SHARDING_REQUIRED')); - }); - this.client.once(Events.READY, () => { - resolve(token); - this.client.clearTimeout(timeout); - }); - }, reject); - } - - destroy() { - this.client.ws.destroy(); - if (this.client.user) this.client.token = null; - } -} - -module.exports = ClientManager; diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 9e44f4260..f349b9bcd 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -19,13 +19,17 @@ class ActionsManager { this.register(require('./GuildRoleCreate')); this.register(require('./GuildRoleDelete')); this.register(require('./GuildRoleUpdate')); + this.register(require('./PresenceUpdate')); this.register(require('./UserUpdate')); + this.register(require('./VoiceStateUpdate')); this.register(require('./GuildEmojiCreate')); this.register(require('./GuildEmojiDelete')); this.register(require('./GuildEmojiUpdate')); this.register(require('./GuildEmojisUpdate')); this.register(require('./GuildRolesPositionUpdate')); this.register(require('./GuildChannelsPositionUpdate')); + this.register(require('./GuildIntegrationsUpdate')); + this.register(require('./WebhooksUpdate')); } register(Action) { diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js new file mode 100644 index 000000000..e9c3bdbf4 --- /dev/null +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -0,0 +1,18 @@ +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class GuildIntegrationsUpdate extends Action { + handle(data) { + const client = this.client; + const guild = client.guilds.get(data.guild_id); + if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); + } +} + +module.exports = GuildIntegrationsUpdate; + +/** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 2e0a1d8cc..febeb73a0 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -2,7 +2,7 @@ const Action = require('./Action'); const { Events, Status } = require('../../util/Constants'); class GuildMemberRemoveAction extends Action { - handle(data) { + handle(data, shard) { const client = this.client; const guild = client.guilds.get(data.guild_id); let member = null; @@ -13,7 +13,7 @@ class GuildMemberRemoveAction extends Action { guild.voiceStates.delete(member.id); member.deleted = true; guild.members.remove(member.id); - if (client.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); + if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } } return { guild, member }; diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index aebc0d389..9308aedb1 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -10,7 +10,9 @@ class MessageCreateAction extends Action { if (existing) return { message: existing }; const message = channel.messages.add(data); const user = message.author; - const member = channel.guild ? channel.guild.member(user) : null; + let member = null; + if (message.member && channel.guild) member = channel.guild.members.add(message.member); + else if (channel.guild) member = channel.guild.member(user); channel.lastMessageID = data.id; if (user) { user.lastMessageID = data.id; diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js new file mode 100644 index 000000000..6da03caf2 --- /dev/null +++ b/src/client/actions/PresenceUpdate.js @@ -0,0 +1,38 @@ +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class PresenceUpdateAction extends Action { + handle(data) { + let cached = this.client.users.get(data.user.id); + if (!cached && data.user.username) cached = this.client.users.add(data.user); + if (!cached) return; + + if (data.user && data.user.username) { + if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data); + } + + const guild = this.client.guilds.get(data.guild_id); + if (!guild) return; + + let member = guild.members.get(cached.id); + if (!member && data.status !== 'offline') { + member = guild.members.add({ user: cached, roles: data.roles, deaf: false, mute: false }); + this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member); + } + + if (member) { + if (this.client.listenerCount(Events.PRESENCE_UPDATE) === 0) { + guild.presences.add(data); + return; + } + const old = member._clone(); + if (member.presence) old.frozenPresence = member.presence._clone(); + guild.presences.add(data); + this.client.emit(Events.PRESENCE_UPDATE, old, member); + } else { + guild.presences.add(data); + } + } +} + +module.exports = PresenceUpdateAction; diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index de796db2d..60adc1746 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -5,19 +5,14 @@ class UserUpdateAction extends Action { handle(data) { const client = this.client; - if (client.user) { - if (client.user.equals(data)) { - return { - old: client.user, - updated: client.user, - }; - } + const newUser = client.users.get(data.user.id); + const oldUser = newUser._update(data.user); - const oldUser = client.user._update(data); - client.emit(Events.USER_UPDATE, oldUser, client.user); + if (!oldUser.equals(newUser)) { + client.emit(Events.USER_UPDATE, oldUser, newUser); return { old: oldUser, - updated: client.user, + updated: newUser, }; } diff --git a/src/client/websocket/packets/handlers/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js similarity index 75% rename from src/client/websocket/packets/handlers/VoiceStateUpdate.js rename to src/client/actions/VoiceStateUpdate.js index e423d0d0b..0eb9c5a57 100644 --- a/src/client/websocket/packets/handlers/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -1,13 +1,10 @@ -const AbstractHandler = require('./AbstractHandler'); - -const { Events } = require('../../../../util/Constants'); -const VoiceState = require('../../../../structures/VoiceState'); - -class VoiceStateUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); +const VoiceState = require('../../structures/VoiceState'); +class VoiceStateUpdate extends Action { + handle(data) { + const client = this.client; const guild = client.guilds.get(data.guild_id); if (guild) { // Update the state @@ -42,4 +39,4 @@ class VoiceStateUpdateHandler extends AbstractHandler { * @param {VoiceState} newState The voice state after the update */ -module.exports = VoiceStateUpdateHandler; +module.exports = VoiceStateUpdate; diff --git a/src/client/websocket/packets/handlers/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js similarity index 57% rename from src/client/websocket/packets/handlers/WebhooksUpdate.js rename to src/client/actions/WebhooksUpdate.js index 7ed2721e3..5ffc41a40 100644 --- a/src/client/websocket/packets/handlers/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -1,10 +1,9 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); -class WebhooksUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; +class WebhooksUpdate extends Action { + handle(data) { + const client = this.client; const channel = client.channels.get(data.channel_id); if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 9bec81509..004cf2019 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -169,7 +169,7 @@ class VoiceConnection extends EventEmitter { self_deaf: false, }, options); - this.client.ws.send({ + this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, }); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 960bc0b75..4e4eec1ee 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,88 +1,281 @@ -const EventEmitter = require('events'); -const { Events, Status } = require('../../util/Constants'); -const WebSocketConnection = require('./WebSocketConnection'); +const WebSocketShard = require('./WebSocketShard'); +const { Events, Status, WSEvents } = require('../../util/Constants'); +const PacketHandlers = require('./handlers'); + +const BeforeReadyWhitelist = [ + WSEvents.READY, + WSEvents.RESUMED, + WSEvents.GUILD_CREATE, + WSEvents.GUILD_DELETE, + WSEvents.GUILD_MEMBERS_CHUNK, + WSEvents.GUILD_MEMBER_ADD, + WSEvents.GUILD_MEMBER_REMOVE, +]; /** * WebSocket Manager of the client. - * @private */ -class WebSocketManager extends EventEmitter { +class WebSocketManager { constructor(client) { - super(); /** * The client that instantiated this WebSocketManager * @type {Client} + * @readonly */ - this.client = client; + Object.defineProperty(this, 'client', { value: client }); /** - * The WebSocket connection of this manager - * @type {?WebSocketConnection} + * The gateway this WebSocketManager uses. + * @type {?string} */ - this.connection = null; + this.gateway = undefined; + + /** + * An array of shards spawned by this WebSocketManager. + * @type {WebSocketShard[]} + */ + this.shards = []; + + /** + * An array of queued shards to be spawned by this WebSocketManager. + * @type {Array} + * @private + */ + this.spawnQueue = []; + + /** + * Whether or not this WebSocketManager is currently spawning shards. + * @type {boolean} + * @private + */ + this.spawning = false; + + /** + * An array of queued events before this WebSocketManager became ready. + * @type {object[]} + * @private + */ + this.packetQueue = []; + + /** + * The current status of this WebSocketManager. + * @type {number} + */ + this.status = Status.IDLE; + + /** + * The current session limit of the client. + * @type {?Object} + * @prop {number} total Total number of identifies available + * @prop {number} remaining Number of identifies remaining + * @prop {number} reset_after Number of milliseconds after which the limit resets + */ + this.sessionStartLimit = null; } /** - * Sends a heartbeat on the available connection. - * @returns {void} + * The average ping of all WebSocketShards + * @type {number} + * @readonly */ - heartbeat() { - if (!this.connection) return this.debug('No connection to heartbeat'); - return this.connection.heartbeat(); + get ping() { + const sum = this.shards.reduce((a, b) => a + b.ping, 0); + return sum / this.shards.length; } /** * Emits a debug event. * @param {string} message Debug message * @returns {void} + * @private */ debug(message) { - return this.client.emit(Events.DEBUG, `[ws] ${message}`); + this.client.emit(Events.DEBUG, `[connection] ${message}`); } /** - * Destroy the client. - * @returns {void} Whether or not destruction was successful + * Handles the session identify rate limit for a shard. + * @param {WebSocketShard} shard Shard to handle + * @private */ - destroy() { - if (!this.connection) { - this.debug('Attempted to destroy WebSocket but no connection exists!'); + async _handleSessionLimit(shard) { + this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit); + const { remaining, reset_after } = this.sessionStartLimit; + if (remaining !== 0) { + this.spawn(); + } else { + shard.debug(`Exceeded identify threshold, setting a timeout for ${reset_after} ms`); + setTimeout(() => this.spawn(), this.sessionStartLimit.reset_after); + } + } + + /** + * Used to spawn WebSocketShards. + * @param {?WebSocketShard|WebSocketShard[]|number|string} query The WebSocketShards to be spawned + * @returns {void} + * @private + */ + spawn(query) { + if (query !== undefined) { + if (Array.isArray(query)) { + for (const item of query) { + if (!this.spawnQueue.includes(item)) this.spawnQueue.push(item); + } + } else if (!this.spawnQueue.includes(query)) { + this.spawnQueue.push(query); + } + } + + if (this.spawning || !this.spawnQueue.length) return; + + this.spawning = true; + let item = this.spawnQueue.shift(); + + if (typeof item === 'string' && !isNaN(item)) item = Number(item); + if (typeof item === 'number') { + const shard = new WebSocketShard(this, item, this.shards[item]); + this.shards[item] = shard; + shard.once(Events.READY, () => { + this.spawning = false; + this.client.setTimeout(() => this._handleSessionLimit(shard), 5000); + }); + shard.once(Events.INVALIDATED, () => { + this.spawning = false; + }); + } else if (item instanceof WebSocketShard) { + item.reconnect(); + } + } + + /** + * Creates a connection to a gateway. + * @param {string} [gateway=this.gateway] The gateway to connect to + * @returns {void} + * @private + */ + connect(gateway = this.gateway) { + this.gateway = gateway; + + if (typeof this.client.options.shards === 'number') { + this.debug('Spawning 1 shard'); + this.spawn(this.client.options.shards); + } else if (Array.isArray(this.client.options.shards)) { + this.debug(`Spawning ${this.client.options.shards.length} shards`); + for (let i = 0; i < this.client.options.shards.length; i++) { + this.spawn(this.client.options.shards[i]); + } + } else { + this.debug(`Spawning ${this.client.options.shardCount} shards`); + for (let i = 0; i < this.client.options.shardCount; i++) { + this.spawn(i); + } + } + } + + /** + * Processes a packet and queues it if this WebSocketManager is not ready. + * @param {Object} packet The packet to be handled + * @param {WebSocketShard} shard The shard that will handle this packet + * @returns {boolean} + * @private + */ + handlePacket(packet, shard) { + if (packet && this.status !== Status.READY) { + if (!BeforeReadyWhitelist.includes(packet.t)) { + this.packetQueue.push({ packet, shardID: shard.id }); + return false; + } + } + + if (this.packetQueue.length) { + const item = this.packetQueue.shift(); + this.client.setImmediate(() => { + this.handlePacket(item.packet, this.shards[item.shardID]); + }); + } + + if (packet && PacketHandlers[packet.t]) { + PacketHandlers[packet.t](this.client, packet, shard); + } + + return false; + } + + /** + * Checks whether the client is ready to be marked as ready. + * @returns {boolean} + * @private + */ + checkReady() { + if (this.shards.filter(s => s).length !== this.client.options.shardCount || + this.shards.some(s => s && s.status !== Status.READY)) { return false; } - return this.connection.destroy(); + + let unavailableGuilds = 0; + for (const guild of this.client.guilds.values()) { + if (!guild.available) unavailableGuilds++; + } + if (unavailableGuilds === 0) { + this.status = Status.NEARLY; + if (!this.client.options.fetchAllMembers) return this.triggerReady(); + // Fetch all members before marking self as ready + const promises = this.client.guilds.map(g => g.members.fetch()); + Promise.all(promises) + .then(() => this.triggerReady()) + .catch(e => { + this.debug(`Failed to fetch all members before ready! ${e}`); + this.triggerReady(); + }); + } + return true; } /** - * Send a packet on the available WebSocket. - * @param {Object} packet Packet to send + * Causes the client to be marked as ready and emits the ready event. * @returns {void} + * @private */ - send(packet) { - if (!this.connection) { - this.debug('No connection to websocket'); + triggerReady() { + if (this.status === Status.READY) { + this.debug('Tried to mark self as ready, but already ready'); return; } - this.connection.send(packet); + this.status = Status.READY; + + /** + * Emitted when the client becomes ready to start working. + * @event Client#ready + */ + this.client.emit(Events.READY); + + this.handlePacket(); } /** - * Connects the client to a gateway. - * @param {string} gateway The gateway to connect to - * @returns {boolean} + * Broadcasts a message to every shard in this WebSocketManager. + * @param {*} packet The packet to send */ - connect(gateway) { - if (!this.connection) { - this.connection = new WebSocketConnection(this, gateway); - return true; + broadcast(packet) { + for (const shard of this.shards) { + if (!shard) continue; + shard.send(packet); } - switch (this.connection.status) { - case Status.IDLE: - case Status.DISCONNECTED: - this.connection.connect(gateway, 5500); - return true; - default: - this.debug(`Couldn't connect to ${gateway} as the websocket is at state ${this.connection.status}`); - return false; + } + + /** + * Destroys all shards. + * @returns {void} + * @private + */ + destroy() { + this.gateway = undefined; + // Lock calls to spawn + this.spawning = true; + + for (const shard of this.shards) { + if (!shard) continue; + shard.destroy(); } } } diff --git a/src/client/websocket/WebSocketConnection.js b/src/client/websocket/WebSocketShard.js similarity index 52% rename from src/client/websocket/WebSocketConnection.js rename to src/client/websocket/WebSocketShard.js index 12e90075c..1c51be0a9 100644 --- a/src/client/websocket/WebSocketConnection.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,25 +1,21 @@ const EventEmitter = require('events'); -const { Events, OPCodes, Status, WSCodes } = require('../../util/Constants'); -const PacketManager = require('./packets/WebSocketPacketManager'); const WebSocket = require('../../WebSocket'); +const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); +let zlib; try { - var zlib = require('zlib-sync'); + zlib = require('zlib-sync'); if (!zlib.Inflate) zlib = require('pako'); } catch (err) { zlib = require('pako'); } /** - * Abstracts a WebSocket connection with decoding/encoding for the Discord gateway. - * @private + * Represents a Shard's Websocket connection. */ -class WebSocketConnection extends EventEmitter { - /** - * @param {WebSocketManager} manager The WebSocket manager - * @param {string} gateway The WebSocket gateway to connect to - */ - constructor(manager, gateway) { +class WebSocketShard extends EventEmitter { + constructor(manager, id, oldShard) { super(); + /** * The WebSocket Manager of this connection * @type {WebSocketManager} @@ -27,242 +23,240 @@ class WebSocketConnection extends EventEmitter { this.manager = manager; /** - * The client this belongs to - * @type {Client} - */ - this.client = manager.client; - - /** - * The WebSocket connection itself - * @type {WebSocket} - */ - this.ws = null; - - /** - * The current sequence of the WebSocket + * The id of the this shard. * @type {number} */ - this.sequence = -1; + this.id = id; /** - * The current sessionID of the WebSocket - * @type {string} - */ - this.sessionID = null; - - /** - * The current status of the client - * @type {number} + * The current status of the shard + * @type {Status} */ this.status = Status.IDLE; /** - * The Packet Manager of the connection - * @type {WebSocketPacketManager} - */ - this.packetManager = new PacketManager(this); - - /** - * The last time a ping was sent (a timestamp) + * The current sequence of the WebSocket * @type {number} + * @private */ - this.lastPingTimestamp = 0; - - /** - * Contains the rate limit queue and metadata - * @type {Object} - */ - this.ratelimit = { - queue: [], - remaining: 120, - total: 120, - time: 60e3, - resetTimer: null, - }; - - /** - * Events that are disabled (will not be processed) - * @type {Object} - */ - this.disabledEvents = {}; - for (const event of this.client.options.disabledEvents) this.disabledEvents[event] = true; + this.sequence = oldShard ? oldShard.sequence : -1; /** * The sequence on WebSocket close * @type {number} + * @private */ this.closeSequence = 0; /** - * Whether or not the WebSocket is expecting to be closed - * @type {boolean} + * The current session id of the WebSocket + * @type {?string} + * @private */ - this.expectingClose = false; + this.sessionID = oldShard && oldShard.sessionID; - this.inflate = null; - this.connect(gateway); - } - - /** - * Causes the client to be marked as ready and emits the ready event. - * @returns {void} - */ - triggerReady() { - if (this.status === Status.READY) { - this.debug('Tried to mark self as ready, but already ready'); - return; - } /** - * Emitted when the client becomes ready to start working. - * @event Client#ready + * Previous heartbeat pings of the websocket (most recent first, limited to three elements) + * @type {number[]} */ - this.status = Status.READY; - this.client.emit(Events.READY); - this.packetManager.handleQueue(); + this.pings = []; + + /** + * The last time a ping was sent (a timestamp) + * @type {number} + * @private + */ + this.lastPingTimestamp = -1; + + /** + * List of servers the shard is connected to + * @type {string[]} + * @private + */ + this.trace = []; + + /** + * Contains the rate limit queue and metadata + * @type {Object} + * @private + */ + this.ratelimit = { + queue: [], + total: 120, + remaining: 120, + time: 60e3, + timer: null, + }; + + /** + * The WebSocket connection for the current shard + * @type {?WebSocket} + * @private + */ + this.ws = null; + + /** + * @external Inflate + * @see {@link https://www.npmjs.com/package/zlib-sync} + */ + + /** + * The compression to use + * @type {?Inflate} + * @private + */ + this.inflate = null; + + this.connect(); } /** - * Checks whether the client is ready to be marked as ready. - * @returns {void} + * Average heartbeat ping of the websocket, obtained by averaging the WebSocketShard#pings property + * @type {number} + * @readonly */ - checkIfReady() { - if (this.status === Status.READY || this.status === Status.NEARLY) return false; - let unavailableGuilds = 0; - for (const guild of this.client.guilds.values()) { - if (!guild.available) unavailableGuilds++; - } - if (unavailableGuilds === 0) { - this.status = Status.NEARLY; - if (!this.client.options.fetchAllMembers) return this.triggerReady(); - // Fetch all members before marking self as ready - const promises = this.client.guilds.map(g => g.members.fetch()); - Promise.all(promises) - .then(() => this.triggerReady()) - .catch(e => { - this.debug(`Failed to fetch all members before ready! ${e}`); - this.triggerReady(); - }); - } - return true; + get ping() { + const sum = this.pings.reduce((a, b) => a + b, 0); + return sum / this.pings.length; } - // Util /** - * Emits a debug message. + * Emits a debug event. * @param {string} message Debug message - * @returns {void} + * @private */ debug(message) { - if (message instanceof Error) message = message.stack; - return this.manager.debug(`[connection] ${message}`); + this.manager.debug(`[shard ${this.id}] ${message}`); } /** - * Processes the current WebSocket queue. + * Sends a heartbeat or sets an interval for sending heartbeats. + * @param {number} [time] If -1, clears the interval, any other number sets an interval + * If no value is given, a heartbeat will be sent instantly + * @private */ - processQueue() { - if (this.ratelimit.remaining === 0) return; - if (this.ratelimit.queue.length === 0) return; - if (this.ratelimit.remaining === this.ratelimit.total) { - this.ratelimit.resetTimer = this.client.setTimeout(() => { - this.ratelimit.remaining = this.ratelimit.total; - this.processQueue(); - }, this.ratelimit.time); - } - while (this.ratelimit.remaining > 0) { - const item = this.ratelimit.queue.shift(); - if (!item) return; - this._send(item); - this.ratelimit.remaining--; - } - } - - /** - * Sends data, bypassing the queue. - * @param {Object} data Packet to send - * @returns {void} - */ - _send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + heartbeat(time) { + if (!isNaN(time)) { + if (time === -1) { + this.debug('Clearing heartbeat interval'); + this.manager.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } else { + this.debug(`Setting a heartbeat interval for ${time}ms`); + this.heartbeatInterval = this.manager.client.setInterval(() => this.heartbeat(), time); + } return; } - this.ws.send(WebSocket.pack(data)); + + this.debug('Sending a heartbeat'); + this.lastPingTimestamp = Date.now(); + this.send({ + op: OPCodes.HEARTBEAT, + d: this.sequence, + }); } /** - * Adds data to the queue to be sent. - * @param {Object} data Packet to send - * @returns {void} + * Acknowledges a heartbeat. + * @private */ - send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); - return; - } - this.ratelimit.queue.push(data); - this.processQueue(); + ackHeartbeat() { + const latency = Date.now() - this.lastPingTimestamp; + this.debug(`Heartbeat acknowledged, latency of ${latency}ms`); + this.pings.unshift(latency); + if (this.pings.length > 3) this.pings.length = 3; } /** - * Creates a connection to a gateway. - * @param {string} gateway The gateway to connect to - * @param {number} [after=0] How long to wait before connecting - * @param {boolean} [force=false] Whether or not to force a new connection even if one already exists - * @returns {boolean} + * Connects the shard to a gateway. + * @private */ - connect(gateway = this.gateway, after = 0, force = false) { - if (after) return this.client.setTimeout(() => this.connect(gateway, 0, force), after); // eslint-disable-line - if (this.ws && !force) { - this.debug('WebSocket connection already exists'); - return false; - } else if (typeof gateway !== 'string') { - this.debug(`Tried to connect to an invalid gateway: ${gateway}`); - return false; - } + connect() { this.inflate = new zlib.Inflate({ chunkSize: 65535, flush: zlib.Z_SYNC_FLUSH, to: WebSocket.encoding === 'json' ? 'string' : '', }); - this.expectingClose = false; - this.gateway = gateway; + const gateway = this.manager.gateway; this.debug(`Connecting to ${gateway}`); const ws = this.ws = WebSocket.create(gateway, { - v: this.client.options.ws.version, + v: this.manager.client.options.ws.version, compress: 'zlib-stream', }); - ws.onmessage = this.onMessage.bind(this); ws.onopen = this.onOpen.bind(this); + ws.onmessage = this.onMessage.bind(this); ws.onerror = this.onError.bind(this); ws.onclose = this.onClose.bind(this); this.status = Status.CONNECTING; - return true; } /** - * Destroys the connection. + * Called whenever a packet is received + * @param {Object} packet Packet received * @returns {boolean} + * @private */ - destroy() { - const ws = this.ws; - if (!ws) { - this.debug('Attempted to destroy WebSocket but no connection exists!'); + onPacket(packet) { + if (!packet) { + this.debug('Received null packet'); return false; } - this.heartbeat(-1); - this.expectingClose = true; - ws.close(1000); - this.packetManager.handleQueue(); - this.ws = null; - this.status = Status.DISCONNECTED; - this.ratelimit.remaining = this.ratelimit.total; - return true; + + this.manager.client.emit(Events.RAW, packet, this.id); + + switch (packet.t) { + case WSEvents.READY: + this.sessionID = packet.d.session_id; + this.trace = packet.d._trace; + this.status = Status.READY; + this.debug(`READY ${this.trace.join(' -> ')} ${this.sessionID}`); + this.heartbeat(); + break; + case WSEvents.RESUMED: { + this.trace = packet.d._trace; + this.status = Status.READY; + const replayed = packet.s - this.sequence; + this.debug(`RESUMED ${this.trace.join(' -> ')} | replayed ${replayed} events.`); + this.heartbeat(); + break; + } + } + + if (packet.s > this.sequence) this.sequence = packet.s; + + switch (packet.op) { + case OPCodes.HELLO: + this.identify(); + return this.heartbeat(packet.d.heartbeat_interval); + case OPCodes.RECONNECT: + return this.reconnect(); + case OPCodes.INVALID_SESSION: + if (!packet.d) this.sessionID = null; + this.sequence = -1; + this.debug('Session invalidated'); + return this.reconnect(Events.INVALIDATED); + case OPCodes.HEARTBEAT_ACK: + return this.ackHeartbeat(); + case OPCodes.HEARTBEAT: + return this.heartbeat(); + default: + return this.manager.handlePacket(packet, this); + } + } + + /** + * Called whenever a connection is opened to the gateway. + * @param {Event} event Received open event + * @private + */ + onOpen() { + this.debug('Connection open'); } /** * Called whenever a message is received. * @param {Event} event Event received + * @private */ onMessage({ data }) { if (data instanceof ArrayBuffer) data = new Uint8Array(data); @@ -278,89 +272,40 @@ class WebSocketConnection extends EventEmitter { let packet; try { packet = WebSocket.unpack(this.inflate.result); + this.manager.client.emit(Events.RAW, packet); } catch (err) { - this.client.emit('debug', err); + this.manager.client.emit(Events.ERROR, err); return; } + if (packet.t === 'READY') { + /** + * Emitted when a shard becomes ready + * @event WebSocketShard#ready + */ + this.emit(Events.READY); + + /** + * Emitted when a shard becomes ready + * @event Client#shardReady + * @param {number} shardID The id of the shard + */ + this.manager.client.emit(Events.SHARD_READY, this.id); + } this.onPacket(packet); - if (this.client.listenerCount('raw')) this.client.emit('raw', packet); - } - - /** - * Sets the current sequence of the connection. - * @param {number} s New sequence - */ - setSequence(s) { - this.sequence = s > this.sequence ? s : this.sequence; - } - - /** - * Called whenever a packet is received. - * @param {Object} packet Received packet - * @returns {boolean} - */ - onPacket(packet) { - if (!packet) { - this.debug('Received null packet'); - return false; - } - switch (packet.op) { - case OPCodes.HELLO: - return this.heartbeat(packet.d.heartbeat_interval); - case OPCodes.RECONNECT: - return this.reconnect(); - case OPCodes.INVALID_SESSION: - if (!packet.d) this.sessionID = null; - this.sequence = -1; - this.debug('Session invalidated -- will identify with a new session'); - return this.identify(packet.d ? 2500 : 0); - case OPCodes.HEARTBEAT_ACK: - return this.ackHeartbeat(); - case OPCodes.HEARTBEAT: - return this.heartbeat(); - default: - return this.packetManager.handle(packet); - } - } - - /** - * Called whenever a connection is opened to the gateway. - * @param {Event} event Received open event - */ - onOpen(event) { - if (event && event.target && event.target.url) this.gateway = event.target.url; - this.debug(`Connected to gateway ${this.gateway}`); - this.identify(); - } - - /** - * Causes a reconnection to the gateway. - */ - reconnect() { - this.debug('Attempting to reconnect in 5500ms...'); - /** - * Emitted whenever the client tries to reconnect to the WebSocket. - * @event Client#reconnecting - */ - this.client.emit(Events.RECONNECTING); - this.connect(this.gateway, 5500, true); } /** * Called whenever an error occurs with the WebSocket. * @param {Error} error The error that occurred + * @private */ onError(error) { if (error && error.message === 'uWs client connection error') { this.reconnect(); return; } - /** - * Emitted whenever the client's WebSocket encounters a connection error. - * @event Client#error - * @param {Error} error The encountered error - */ - this.client.emit(Events.ERROR, error); + this.emit(Events.INVALIDATED); + this.manager.client.emit(Events.ERROR, error); } /** @@ -371,90 +316,50 @@ class WebSocketConnection extends EventEmitter { /** * Called whenever a connection to the gateway is closed. * @param {CloseEvent} event Close event that was received + * @private */ onClose(event) { - this.debug(`${this.expectingClose ? 'Client' : 'Server'} closed the WebSocket connection: ${event.code}`); this.closeSequence = this.sequence; - // Reset the state before trying to fix anything this.emit('close', event); - this.heartbeat(-1); - // Should we reconnect? if (event.code === 1000 ? this.expectingClose : WSCodes[event.code]) { - this.expectingClose = false; /** * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. * @event Client#disconnect * @param {CloseEvent} event The WebSocket close event + * @param {number} shardID The shard that disconnected */ - this.client.emit(Events.DISCONNECT, event); + this.manager.client.emit(Events.DISCONNECT, event, this.id); + this.debug(WSCodes[event.code]); - this.destroy(); return; } - this.expectingClose = false; - this.reconnect(); + this.reconnect(Events.INVALIDATED); } - // Heartbeat - /** - * Acknowledges a heartbeat. - */ - ackHeartbeat() { - this.debug(`Heartbeat acknowledged, latency of ${Date.now() - this.lastPingTimestamp}ms`); - this.client._pong(this.lastPingTimestamp); - } - - /** - * Sends a heartbeat or sets an interval for sending heartbeats. - * @param {number} [time] If -1, clears the interval, any other number sets an interval - * If no value is given, a heartbeat will be sent instantly - */ - heartbeat(time) { - if (!isNaN(time)) { - if (time === -1) { - this.debug('Clearing heartbeat interval'); - this.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } else { - this.debug(`Setting a heartbeat interval for ${time}ms`); - this.heartbeatInterval = this.client.setInterval(() => this.heartbeat(), time); - } - return; - } - this.debug('Sending a heartbeat'); - this.lastPingTimestamp = Date.now(); - this.send({ - op: OPCodes.HEARTBEAT, - d: this.sequence, - }); - } - - // Identification /** * Identifies the client on a connection. - * @param {number} [after] How long to wait before identifying * @returns {void} + * @private */ - identify(after) { - if (after) return this.client.setTimeout(this.identify.bind(this), after); + identify() { return this.sessionID ? this.identifyResume() : this.identifyNew(); } /** * Identifies as a new connection on the gateway. * @returns {void} + * @private */ identifyNew() { - if (!this.client.token) { + if (!this.manager.client.token) { this.debug('No token available to identify a new session with'); return; } // Clone the generic payload and assign the token - const d = Object.assign({ token: this.client.token }, this.client.options.ws); + const d = Object.assign({ token: this.manager.client.token }, this.manager.client.options.ws); - // Sharding stuff - const { shardId, shardCount } = this.client.options; - if (shardCount > 0) d.shard = [Number(shardId), Number(shardCount)]; + const { totalShardCount } = this.manager.client.options; + d.shard = [this.id, Number(totalShardCount)]; // Send the payload this.debug('Identifying as a new session'); @@ -464,6 +369,7 @@ class WebSocketConnection extends EventEmitter { /** * Resumes a session on the gateway. * @returns {void} + * @private */ identifyResume() { if (!this.sessionID) { @@ -473,7 +379,7 @@ class WebSocketConnection extends EventEmitter { this.debug(`Attempting to resume session ${this.sessionID}`); const d = { - token: this.client.token, + token: this.manager.client.token, session_id: this.sessionID, seq: this.sequence, }; @@ -483,6 +389,85 @@ class WebSocketConnection extends EventEmitter { d, }); } -} -module.exports = WebSocketConnection; + /** + * Adds data to the queue to be sent. + * @param {Object} data Packet to send + * @returns {void} + */ + send(data) { + this.ratelimit.queue.push(data); + this.processQueue(); + } + + /** + * Sends data, bypassing the queue. + * @param {Object} data Packet to send + * @returns {void} + * @private + */ + _send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + return; + } + this.ws.send(WebSocket.pack(data)); + } + + /** + * Processes the current WebSocket queue. + * @returns {void} + * @private + */ + processQueue() { + if (this.ratelimit.remaining === 0) return; + if (this.ratelimit.queue.length === 0) return; + if (this.ratelimit.remaining === this.ratelimit.total) { + this.ratelimit.resetTimer = this.manager.client.setTimeout(() => { + this.ratelimit.remaining = this.ratelimit.total; + this.processQueue(); + }, this.ratelimit.time); + } + while (this.ratelimit.remaining > 0) { + const item = this.ratelimit.queue.shift(); + if (!item) return; + this._send(item); + this.ratelimit.remaining--; + } + } + + /** + * Triggers a shard reconnect. + * @param {?string} [event] The event for the shard to emit + * @returns {void} + * @private + */ + reconnect(event) { + this.heartbeat(-1); + this.status = Status.RECONNECTING; + + /** + * Emitted whenever a shard tries to reconnect to the WebSocket. + * @event Client#reconnecting + */ + this.manager.client.emit(Events.RECONNECTING, this.id); + + if (event === Events.INVALIDATED) this.emit(event); + this.manager.spawn(this.id); + } + + /** + * Destroys the current shard and terminates its connection. + * @returns {void} + * @private + */ + destroy() { + this.heartbeat(-1); + this.expectingClose = true; + if (this.ws) this.ws.close(1000); + this.ws = null; + this.status = Status.DISCONNECTED; + this.ratelimit.remaining = this.ratelimit.total; + } +} +module.exports = WebSocketShard; diff --git a/src/client/websocket/handlers/CHANNEL_CREATE.js b/src/client/websocket/handlers/CHANNEL_CREATE.js new file mode 100644 index 000000000..3074254fc --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.ChannelCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/CHANNEL_DELETE.js b/src/client/websocket/handlers/CHANNEL_DELETE.js new file mode 100644 index 000000000..158ccb352 --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.ChannelDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js new file mode 100644 index 000000000..1272dbbb1 --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -0,0 +1,20 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.get(data.channel_id); + const time = new Date(data.last_pin_timestamp); + + if (channel && time) { + // Discord sends null for last_pin_timestamp if the last pinned message was removed + channel.lastPinTimestamp = time.getTime() || null; + + /** + * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, + * not much information can be provided easily here - you need to manually check the pins yourself. + * @event Client#channelPinsUpdate + * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in + * @param {Date} time The time of the pins update + */ + client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); + } +}; diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js new file mode 100644 index 000000000..46d9037a2 --- /dev/null +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -0,0 +1,15 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { old, updated } = client.actions.ChannelUpdate.handle(packet.d); + if (old && updated) { + /** + * Emitted whenever a channel is updated - e.g. name change, topic change. + * @event Client#channelUpdate + * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update + * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update + */ + client.emit(Events.CHANNEL_UPDATE, old, updated); + } +}; + diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js new file mode 100644 index 000000000..00772c8cc --- /dev/null +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -0,0 +1,14 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.get(data.guild_id); + const user = client.users.get(data.user.id); + + /** + * Emitted whenever a member is banned from a guild. + * @event Client#guildBanAdd + * @param {Guild} guild The guild that the ban occurred in + * @param {User} user The user that was banned + */ + if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); +}; diff --git a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js new file mode 100644 index 000000000..08483a835 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildBanRemove.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js new file mode 100644 index 000000000..05250ed6e --- /dev/null +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -0,0 +1,26 @@ +const { Events, Status } = require('../../../util/Constants'); + +module.exports = async (client, { d: data }, shard) => { + let guild = client.guilds.get(data.id); + if (guild) { + if (!guild.available && !data.unavailable) { + // A newly available guild + guild._patch(data); + client.ws.checkReady(); + } + } else { + // A new guild + data.shardID = shard.id; + guild = client.guilds.add(data); + const emitEvent = client.ws.status === Status.READY; + if (emitEvent) { + /** + * Emitted whenever the client joins a guild. + * @event Client#guildCreate + * @param {Guild} guild The created guild + */ + if (client.options.fetchAllMembers) await guild.members.fetch(); + client.emit(Events.GUILD_CREATE, guild); + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_DELETE.js b/src/client/websocket/handlers/GUILD_DELETE.js new file mode 100644 index 000000000..19d1b3b0a --- /dev/null +++ b/src/client/websocket/handlers/GUILD_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js new file mode 100644 index 000000000..5fa5a9d54 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildEmojisUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js new file mode 100644 index 000000000..6c1a0cfd3 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildIntegrationsUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js new file mode 100644 index 000000000..7178264d2 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -0,0 +1,17 @@ +const { Events } = require('../../../util/Constants'); +const Collection = require('../../../util/Collection'); + +module.exports = (client, { d: data }) => { + const guild = client.guilds.get(data.guild_id); + if (!guild) return; + const members = new Collection(); + + for (const member of data.members) members.set(member.user.id, guild.members.add(member)); + /** + * Emitted whenever a chunk of guild members is received (all members come from the same guild). + * @event Client#guildMembersChunk + * @param {Collection} members The members in the chunk + * @param {Guild} guild The guild related to the member chunk + */ + client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js new file mode 100644 index 000000000..367058a5b --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -0,0 +1,17 @@ +const { Events, Status } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.get(data.guild_id); + if (guild) { + guild.memberCount++; + const member = guild.members.add(data); + if (shard.status === Status.READY) { + /** + * Emitted whenever a user joins a guild. + * @event Client#guildMemberAdd + * @param {GuildMember} member The member that has joined a guild + */ + client.emit(Events.GUILD_MEMBER_ADD, member); + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js new file mode 100644 index 000000000..b00da0e63 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet, shard) => { + client.actions.GuildMemberRemove.handle(packet.d, shard); +}; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js new file mode 100644 index 000000000..be4f573ad --- /dev/null +++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -0,0 +1,20 @@ +const { Status, Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }, shard) => { + const guild = client.guilds.get(data.guild_id); + if (guild) { + const member = guild.members.get(data.user.id); + if (member) { + const old = member._update(data); + if (shard.status === Status.READY) { + /** + * Emitted whenever a guild member changes - i.e. new role, removed role, nickname. + * @event Client#guildMemberUpdate + * @param {GuildMember} oldMember The member before the update + * @param {GuildMember} newMember The member after the update + */ + client.emit(Events.GUILD_MEMBER_UPDATE, old, member); + } + } + } +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js new file mode 100644 index 000000000..b6ea80381 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js new file mode 100644 index 000000000..d1093cb27 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js new file mode 100644 index 000000000..c1f526c57 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildRoleUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_SYNC.js b/src/client/websocket/handlers/GUILD_SYNC.js new file mode 100644 index 000000000..f27da424e --- /dev/null +++ b/src/client/websocket/handlers/GUILD_SYNC.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildSync.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/GUILD_UPDATE.js b/src/client/websocket/handlers/GUILD_UPDATE.js new file mode 100644 index 000000000..0f3e24f74 --- /dev/null +++ b/src/client/websocket/handlers/GUILD_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.GuildUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_CREATE.js b/src/client/websocket/handlers/MESSAGE_CREATE.js new file mode 100644 index 000000000..bc9303fd5 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_CREATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE.js b/src/client/websocket/handlers/MESSAGE_DELETE.js new file mode 100644 index 000000000..09062196c --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_DELETE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageDelete.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js new file mode 100644 index 000000000..a927b3b14 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageDeleteBulk.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js new file mode 100644 index 000000000..d81762124 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -0,0 +1,6 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { user, reaction } = client.actions.MessageReactionAdd.handle(packet.d); + if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js new file mode 100644 index 000000000..8b9f22a17 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageReactionRemove.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js new file mode 100644 index 000000000..2323cfe05 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.MessageReactionRemoveAll.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/MESSAGE_UPDATE.js b/src/client/websocket/handlers/MESSAGE_UPDATE.js new file mode 100644 index 000000000..9be750c7e --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_UPDATE.js @@ -0,0 +1,14 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet) => { + const { old, updated } = client.actions.MessageUpdate.handle(packet.d); + if (old && updated) { + /** + * Emitted whenever a message is updated - e.g. embed or content change. + * @event Client#messageUpdate + * @param {Message} oldMessage The message before the update + * @param {Message} newMessage The message after the update + */ + client.emit(Events.MESSAGE_UPDATE, old, updated); + } +}; diff --git a/src/client/websocket/handlers/PRESENCE_UPDATE.js b/src/client/websocket/handlers/PRESENCE_UPDATE.js new file mode 100644 index 000000000..89b9f0e25 --- /dev/null +++ b/src/client/websocket/handlers/PRESENCE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.PresenceUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js new file mode 100644 index 000000000..f22968d3d --- /dev/null +++ b/src/client/websocket/handlers/READY.js @@ -0,0 +1,16 @@ +let ClientUser; + +module.exports = (client, { d: data }, shard) => { + if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.readyAt = new Date(); + client.users.set(clientUser.id, clientUser); + + for (const guild of data.guilds) { + guild.shardID = shard.id; + client.guilds.add(guild); + } + + client.ws.checkReady(); +}; diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js new file mode 100644 index 000000000..6cc355e98 --- /dev/null +++ b/src/client/websocket/handlers/RESUMED.js @@ -0,0 +1,12 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, packet, shard) => { + const replayed = shard.sequence - shard.closeSequence; + /** + * Emitted when the client gateway resumes. + * @event Client#resume + * @param {number} replayed The number of events that were replayed + * @param {number} shardID The ID of the shard that resumed + */ + client.emit(Events.RESUMED, replayed, shard.id); +}; diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js new file mode 100644 index 000000000..ac01d30d9 --- /dev/null +++ b/src/client/websocket/handlers/TYPING_START.js @@ -0,0 +1,16 @@ +const { Events } = require('../../../util/Constants'); + +module.exports = (client, { d: data }) => { + const channel = client.channels.get(data.channel_id); + const user = client.users.get(data.user_id); + + if (channel && user) { + /** + * Emitted whenever a user starts typing in a channel. + * @event Client#typingStart + * @param {Channel} channel The channel the user started typing in + * @param {User} user The user that started typing + */ + client.emit(Events.TYPING_START, channel, user); + } +}; diff --git a/src/client/websocket/handlers/USER_UPDATE.js b/src/client/websocket/handlers/USER_UPDATE.js new file mode 100644 index 000000000..3c5b859c3 --- /dev/null +++ b/src/client/websocket/handlers/USER_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.UserUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js new file mode 100644 index 000000000..c8ac3883c --- /dev/null +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.emit('self.voiceServer', packet.d); +}; diff --git a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js new file mode 100644 index 000000000..a9527adaa --- /dev/null +++ b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.VoiceStateUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js new file mode 100644 index 000000000..b1afb91c5 --- /dev/null +++ b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js @@ -0,0 +1,3 @@ +module.exports = (client, packet) => { + client.actions.WebhooksUpdate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js new file mode 100644 index 000000000..77bcf8fd0 --- /dev/null +++ b/src/client/websocket/handlers/index.js @@ -0,0 +1,11 @@ +const { WSEvents } = require('../../../util/Constants'); + +const handlers = {}; + +for (const name of Object.keys(WSEvents)) { + try { + handlers[name] = require(`./${name}.js`); + } catch (err) {} // eslint-disable-line no-empty +} + +module.exports = handlers; diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js deleted file mode 100644 index a58871380..000000000 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ /dev/null @@ -1,104 +0,0 @@ -const { OPCodes, Status, WSEvents } = require('../../../util/Constants'); - -const BeforeReadyWhitelist = [ - WSEvents.READY, - WSEvents.RESUMED, - WSEvents.GUILD_CREATE, - WSEvents.GUILD_DELETE, - WSEvents.GUILD_MEMBERS_CHUNK, - WSEvents.GUILD_MEMBER_ADD, - WSEvents.GUILD_MEMBER_REMOVE, -]; - -class WebSocketPacketManager { - constructor(connection) { - this.ws = connection; - this.handlers = {}; - this.queue = []; - - this.register(WSEvents.READY, require('./handlers/Ready')); - this.register(WSEvents.RESUMED, require('./handlers/Resumed')); - this.register(WSEvents.GUILD_CREATE, require('./handlers/GuildCreate')); - this.register(WSEvents.GUILD_DELETE, require('./handlers/GuildDelete')); - this.register(WSEvents.GUILD_UPDATE, require('./handlers/GuildUpdate')); - this.register(WSEvents.GUILD_BAN_ADD, require('./handlers/GuildBanAdd')); - this.register(WSEvents.GUILD_BAN_REMOVE, require('./handlers/GuildBanRemove')); - this.register(WSEvents.GUILD_MEMBER_ADD, require('./handlers/GuildMemberAdd')); - this.register(WSEvents.GUILD_MEMBER_REMOVE, require('./handlers/GuildMemberRemove')); - this.register(WSEvents.GUILD_MEMBER_UPDATE, require('./handlers/GuildMemberUpdate')); - this.register(WSEvents.GUILD_ROLE_CREATE, require('./handlers/GuildRoleCreate')); - this.register(WSEvents.GUILD_ROLE_DELETE, require('./handlers/GuildRoleDelete')); - this.register(WSEvents.GUILD_ROLE_UPDATE, require('./handlers/GuildRoleUpdate')); - this.register(WSEvents.GUILD_EMOJIS_UPDATE, require('./handlers/GuildEmojisUpdate')); - this.register(WSEvents.GUILD_MEMBERS_CHUNK, require('./handlers/GuildMembersChunk')); - this.register(WSEvents.GUILD_INTEGRATIONS_UPDATE, require('./handlers/GuildIntegrationsUpdate')); - this.register(WSEvents.CHANNEL_CREATE, require('./handlers/ChannelCreate')); - this.register(WSEvents.CHANNEL_DELETE, require('./handlers/ChannelDelete')); - this.register(WSEvents.CHANNEL_UPDATE, require('./handlers/ChannelUpdate')); - this.register(WSEvents.CHANNEL_PINS_UPDATE, require('./handlers/ChannelPinsUpdate')); - this.register(WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate')); - this.register(WSEvents.USER_UPDATE, require('./handlers/UserUpdate')); - this.register(WSEvents.VOICE_STATE_UPDATE, require('./handlers/VoiceStateUpdate')); - this.register(WSEvents.TYPING_START, require('./handlers/TypingStart')); - this.register(WSEvents.MESSAGE_CREATE, require('./handlers/MessageCreate')); - this.register(WSEvents.MESSAGE_DELETE, require('./handlers/MessageDelete')); - this.register(WSEvents.MESSAGE_UPDATE, require('./handlers/MessageUpdate')); - this.register(WSEvents.MESSAGE_DELETE_BULK, require('./handlers/MessageDeleteBulk')); - this.register(WSEvents.VOICE_SERVER_UPDATE, require('./handlers/VoiceServerUpdate')); - this.register(WSEvents.MESSAGE_REACTION_ADD, require('./handlers/MessageReactionAdd')); - this.register(WSEvents.MESSAGE_REACTION_REMOVE, require('./handlers/MessageReactionRemove')); - this.register(WSEvents.MESSAGE_REACTION_REMOVE_ALL, require('./handlers/MessageReactionRemoveAll')); - this.register(WSEvents.WEBHOOKS_UPDATE, require('./handlers/WebhooksUpdate')); - } - - get client() { - return this.ws.client; - } - - register(event, Handler) { - this.handlers[event] = new Handler(this); - } - - handleQueue() { - this.queue.forEach((element, index) => { - this.handle(this.queue[index], true); - this.queue.splice(index, 1); - }); - } - - handle(packet, queue = false) { - if (packet.op === OPCodes.HEARTBEAT_ACK) { - this.ws.client._pong(this.ws.client._pingTimestamp); - this.ws.lastHeartbeatAck = true; - this.ws.client.emit('debug', 'Heartbeat acknowledged'); - } else if (packet.op === OPCodes.HEARTBEAT) { - this.client.ws.send({ - op: OPCodes.HEARTBEAT, - d: this.client.ws.sequence, - }); - this.ws.client.emit('debug', 'Received gateway heartbeat'); - } - - if (this.ws.status === Status.RECONNECTING) { - this.ws.reconnecting = false; - this.ws.checkIfReady(); - } - - this.ws.setSequence(packet.s); - - if (this.ws.disabledEvents[packet.t] !== undefined) return false; - - if (this.ws.status !== Status.READY) { - if (BeforeReadyWhitelist.indexOf(packet.t) === -1) { - this.queue.push(packet); - return false; - } - } - - if (!queue && this.queue.length > 0) this.handleQueue(); - if (this.handlers[packet.t]) return this.handlers[packet.t].handle(packet); - return false; - } -} - -module.exports = WebSocketPacketManager; diff --git a/src/client/websocket/packets/handlers/AbstractHandler.js b/src/client/websocket/packets/handlers/AbstractHandler.js deleted file mode 100644 index c1c2a5a2d..000000000 --- a/src/client/websocket/packets/handlers/AbstractHandler.js +++ /dev/null @@ -1,11 +0,0 @@ -class AbstractHandler { - constructor(packetManager) { - this.packetManager = packetManager; - } - - handle(packet) { - return packet; - } -} - -module.exports = AbstractHandler; diff --git a/src/client/websocket/packets/handlers/ChannelCreate.js b/src/client/websocket/packets/handlers/ChannelCreate.js deleted file mode 100644 index 5ccc05708..000000000 --- a/src/client/websocket/packets/handlers/ChannelCreate.js +++ /dev/null @@ -1,15 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class ChannelCreateHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.ChannelCreate.handle(packet.d); - } -} - -module.exports = ChannelCreateHandler; - -/** - * Emitted whenever a channel is created. - * @event Client#channelCreate - * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created - */ diff --git a/src/client/websocket/packets/handlers/ChannelDelete.js b/src/client/websocket/packets/handlers/ChannelDelete.js deleted file mode 100644 index 68eb9a903..000000000 --- a/src/client/websocket/packets/handlers/ChannelDelete.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class ChannelDeleteHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.ChannelDelete.handle(packet.d); - } -} - -module.exports = ChannelDeleteHandler; diff --git a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js b/src/client/websocket/packets/handlers/ChannelPinsUpdate.js deleted file mode 100644 index b8cb64019..000000000 --- a/src/client/websocket/packets/handlers/ChannelPinsUpdate.js +++ /dev/null @@ -1,37 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -/* -{ t: 'CHANNEL_PINS_UPDATE', - s: 666, - op: 0, - d: - { last_pin_timestamp: '2016-08-28T17:37:13.171774+00:00', - channel_id: '314866471639044027' } } -*/ - -class ChannelPinsUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const channel = client.channels.get(data.channel_id); - const time = new Date(data.last_pin_timestamp); - if (channel && time) { - // Discord sends null for last_pin_timestamp if the last pinned message was removed - channel.lastPinTimestamp = time.getTime() || null; - - client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); - } - } -} - -module.exports = ChannelPinsUpdate; - -/** - * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, not much information - * can be provided easily here - you need to manually check the pins yourself. - * The `time` parameter will be a Unix Epoch Date object when there are no pins left. - * @event Client#channelPinsUpdate - * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occurred in - * @param {Date} time The time when the last pinned message was pinned - */ diff --git a/src/client/websocket/packets/handlers/ChannelUpdate.js b/src/client/websocket/packets/handlers/ChannelUpdate.js deleted file mode 100644 index f0d1873a0..000000000 --- a/src/client/websocket/packets/handlers/ChannelUpdate.js +++ /dev/null @@ -1,20 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class ChannelUpdateHandler extends AbstractHandler { - handle(packet) { - const { old, updated } = this.packetManager.client.actions.ChannelUpdate.handle(packet.d); - if (old && updated) { - this.packetManager.client.emit(Events.CHANNEL_UPDATE, old, updated); - } - } -} - -module.exports = ChannelUpdateHandler; - -/** - * Emitted whenever a channel is updated - e.g. name change, topic change. - * @event Client#channelUpdate - * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update - * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update - */ diff --git a/src/client/websocket/packets/handlers/GuildBanAdd.js b/src/client/websocket/packets/handlers/GuildBanAdd.js deleted file mode 100644 index 89f57b78d..000000000 --- a/src/client/websocket/packets/handlers/GuildBanAdd.js +++ /dev/null @@ -1,23 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class GuildBanAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - const user = client.users.add(data.user); - if (guild && user) client.emit(Events.GUILD_BAN_ADD, guild, user); - } -} - -/** - * Emitted whenever a member is banned from a guild. - * @event Client#guildBanAdd - * @param {Guild} guild The guild that the ban occurred in - * @param {User} user The user that was banned - */ - -module.exports = GuildBanAddHandler; diff --git a/src/client/websocket/packets/handlers/GuildBanRemove.js b/src/client/websocket/packets/handlers/GuildBanRemove.js deleted file mode 100644 index c4edbdeb6..000000000 --- a/src/client/websocket/packets/handlers/GuildBanRemove.js +++ /dev/null @@ -1,20 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); - -class GuildBanRemoveHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildBanRemove.handle(data); - } -} - -/** - * Emitted whenever a member is unbanned from a guild. - * @event Client#guildBanRemove - * @param {Guild} guild The guild that the unban occurred in - * @param {User} user The user that was unbanned - */ - -module.exports = GuildBanRemoveHandler; diff --git a/src/client/websocket/packets/handlers/GuildCreate.js b/src/client/websocket/packets/handlers/GuildCreate.js deleted file mode 100644 index 96c5ae987..000000000 --- a/src/client/websocket/packets/handlers/GuildCreate.js +++ /dev/null @@ -1,33 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildCreateHandler extends AbstractHandler { - async handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - - let guild = client.guilds.get(data.id); - if (guild) { - if (!guild.available && !data.unavailable) { - // A newly available guild - guild._patch(data); - this.packetManager.ws.checkIfReady(); - } - } else { - // A new guild - guild = client.guilds.add(data); - const emitEvent = client.ws.connection.status === Status.READY; - if (emitEvent) { - /** - * Emitted whenever the client joins a guild. - * @event Client#guildCreate - * @param {Guild} guild The created guild - */ - if (client.options.fetchAllMembers) await guild.members.fetch(); - client.emit(Events.GUILD_CREATE, guild); - } - } - } -} - -module.exports = GuildCreateHandler; diff --git a/src/client/websocket/packets/handlers/GuildDelete.js b/src/client/websocket/packets/handlers/GuildDelete.js deleted file mode 100644 index 58d60bc1a..000000000 --- a/src/client/websocket/packets/handlers/GuildDelete.js +++ /dev/null @@ -1,16 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildDeleteHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - client.actions.GuildDelete.handle(packet.d); - } -} - -/** - * Emitted whenever a guild kicks the client or the guild is deleted/left. - * @event Client#guildDelete - * @param {Guild} guild The guild that was deleted - */ - -module.exports = GuildDeleteHandler; diff --git a/src/client/websocket/packets/handlers/GuildEmojisUpdate.js b/src/client/websocket/packets/handlers/GuildEmojisUpdate.js deleted file mode 100644 index 2906e74fc..000000000 --- a/src/client/websocket/packets/handlers/GuildEmojisUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildEmojisUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildEmojisUpdate.handle(data); - } -} - -module.exports = GuildEmojisUpdate; diff --git a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js b/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js deleted file mode 100644 index 5adfb5b0f..000000000 --- a/src/client/websocket/packets/handlers/GuildIntegrationsUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class GuildIntegrationsHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); - } -} - -module.exports = GuildIntegrationsHandler; - -/** - * Emitted whenever a guild integration is updated - * @event Client#guildIntegrationsUpdate - * @param {Guild} guild The guild whose integrations were updated - */ diff --git a/src/client/websocket/packets/handlers/GuildMemberAdd.js b/src/client/websocket/packets/handlers/GuildMemberAdd.js deleted file mode 100644 index 15201b825..000000000 --- a/src/client/websocket/packets/handlers/GuildMemberAdd.js +++ /dev/null @@ -1,27 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildMemberAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) { - guild.memberCount++; - const member = guild.members.add(data); - if (client.ws.connection.status === Status.READY) { - client.emit(Events.GUILD_MEMBER_ADD, member); - } - } - } -} - -module.exports = GuildMemberAddHandler; - -/** - * Emitted whenever a user joins a guild. - * @event Client#guildMemberAdd - * @param {GuildMember} member The member that has joined a guild - */ diff --git a/src/client/websocket/packets/handlers/GuildMemberRemove.js b/src/client/websocket/packets/handlers/GuildMemberRemove.js deleted file mode 100644 index 6ec1bfe64..000000000 --- a/src/client/websocket/packets/handlers/GuildMemberRemove.js +++ /dev/null @@ -1,13 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); - -class GuildMemberRemoveHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildMemberRemove.handle(data); - } -} - -module.exports = GuildMemberRemoveHandler; diff --git a/src/client/websocket/packets/handlers/GuildMemberUpdate.js b/src/client/websocket/packets/handlers/GuildMemberUpdate.js deleted file mode 100644 index 901893690..000000000 --- a/src/client/websocket/packets/handlers/GuildMemberUpdate.js +++ /dev/null @@ -1,29 +0,0 @@ -// ##untested handler## - -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class GuildMemberUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (guild) { - const member = guild.members.get(data.user.id); - if (member) { - const old = member._update(data); - if (client.ws.connection.status === Status.READY) { - /** - * Emitted whenever a guild member's details (e.g. role, nickname) are changed - * @event Client#guildMemberUpdate - * @param {GuildMember} oldMember The member before the update - * @param {GuildMember} newMember The member after the update - */ - client.emit(Events.GUILD_MEMBER_UPDATE, old, member); - } - } - } - } -} - -module.exports = GuildMemberUpdateHandler; diff --git a/src/client/websocket/packets/handlers/GuildMembersChunk.js b/src/client/websocket/packets/handlers/GuildMembersChunk.js deleted file mode 100644 index 4e821f5cc..000000000 --- a/src/client/websocket/packets/handlers/GuildMembersChunk.js +++ /dev/null @@ -1,28 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); -const Collection = require('../../../../util/Collection'); - -class GuildMembersChunkHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const guild = client.guilds.get(data.guild_id); - if (!guild) return; - const members = new Collection(); - - for (const member of data.members) members.set(member.user.id, guild.members.add(member)); - - client.emit(Events.GUILD_MEMBERS_CHUNK, members, guild); - - client.ws.lastHeartbeatAck = true; - } -} - -/** - * Emitted whenever a chunk of guild members is received (all members come from the same guild). - * @event Client#guildMembersChunk - * @param {Collection} members The members in the chunk - * @param {Guild} guild The guild related to the member chunk - */ - -module.exports = GuildMembersChunkHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleCreate.js b/src/client/websocket/packets/handlers/GuildRoleCreate.js deleted file mode 100644 index 8581d53f6..000000000 --- a/src/client/websocket/packets/handlers/GuildRoleCreate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleCreateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleCreate.handle(data); - } -} - -module.exports = GuildRoleCreateHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleDelete.js b/src/client/websocket/packets/handlers/GuildRoleDelete.js deleted file mode 100644 index 63439b0fe..000000000 --- a/src/client/websocket/packets/handlers/GuildRoleDelete.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleDeleteHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleDelete.handle(data); - } -} - -module.exports = GuildRoleDeleteHandler; diff --git a/src/client/websocket/packets/handlers/GuildRoleUpdate.js b/src/client/websocket/packets/handlers/GuildRoleUpdate.js deleted file mode 100644 index 6fbdc109a..000000000 --- a/src/client/websocket/packets/handlers/GuildRoleUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildRoleUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildRoleUpdate.handle(data); - } -} - -module.exports = GuildRoleUpdateHandler; diff --git a/src/client/websocket/packets/handlers/GuildUpdate.js b/src/client/websocket/packets/handlers/GuildUpdate.js deleted file mode 100644 index 70eff52c4..000000000 --- a/src/client/websocket/packets/handlers/GuildUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class GuildUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.GuildUpdate.handle(data); - } -} - -module.exports = GuildUpdateHandler; diff --git a/src/client/websocket/packets/handlers/MessageCreate.js b/src/client/websocket/packets/handlers/MessageCreate.js deleted file mode 100644 index 31f5f2806..000000000 --- a/src/client/websocket/packets/handlers/MessageCreate.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageCreateHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageCreate.handle(packet.d); - } -} - -module.exports = MessageCreateHandler; diff --git a/src/client/websocket/packets/handlers/MessageDelete.js b/src/client/websocket/packets/handlers/MessageDelete.js deleted file mode 100644 index 831a0ae06..000000000 --- a/src/client/websocket/packets/handlers/MessageDelete.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageDeleteHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageDelete.handle(packet.d); - } -} - -module.exports = MessageDeleteHandler; diff --git a/src/client/websocket/packets/handlers/MessageDeleteBulk.js b/src/client/websocket/packets/handlers/MessageDeleteBulk.js deleted file mode 100644 index 0077dbf81..000000000 --- a/src/client/websocket/packets/handlers/MessageDeleteBulk.js +++ /dev/null @@ -1,9 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageDeleteBulkHandler extends AbstractHandler { - handle(packet) { - this.packetManager.client.actions.MessageDeleteBulk.handle(packet.d); - } -} - -module.exports = MessageDeleteBulkHandler; diff --git a/src/client/websocket/packets/handlers/MessageReactionAdd.js b/src/client/websocket/packets/handlers/MessageReactionAdd.js deleted file mode 100644 index 34e5c6138..000000000 --- a/src/client/websocket/packets/handlers/MessageReactionAdd.js +++ /dev/null @@ -1,13 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class MessageReactionAddHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const { user, reaction } = client.actions.MessageReactionAdd.handle(data); - if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); - } -} - -module.exports = MessageReactionAddHandler; diff --git a/src/client/websocket/packets/handlers/MessageReactionRemove.js b/src/client/websocket/packets/handlers/MessageReactionRemove.js deleted file mode 100644 index cddde7033..000000000 --- a/src/client/websocket/packets/handlers/MessageReactionRemove.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageReactionRemove extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.MessageReactionRemove.handle(data); - } -} - -module.exports = MessageReactionRemove; diff --git a/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js b/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js deleted file mode 100644 index 303da9ca0..000000000 --- a/src/client/websocket/packets/handlers/MessageReactionRemoveAll.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class MessageReactionRemoveAll extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.MessageReactionRemoveAll.handle(data); - } -} - -module.exports = MessageReactionRemoveAll; diff --git a/src/client/websocket/packets/handlers/MessageUpdate.js b/src/client/websocket/packets/handlers/MessageUpdate.js deleted file mode 100644 index 33e45b19b..000000000 --- a/src/client/websocket/packets/handlers/MessageUpdate.js +++ /dev/null @@ -1,20 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class MessageUpdateHandler extends AbstractHandler { - handle(packet) { - const { old, updated } = this.packetManager.client.actions.MessageUpdate.handle(packet.d); - if (old && updated) { - this.packetManager.client.emit(Events.MESSAGE_UPDATE, old, updated); - } - } -} - -module.exports = MessageUpdateHandler; - -/** - * Emitted whenever a message is updated - e.g. embed or content change. - * @event Client#messageUpdate - * @param {Message} oldMessage The message before the update - * @param {Message} newMessage The message after the update - */ diff --git a/src/client/websocket/packets/handlers/PresenceUpdate.js b/src/client/websocket/packets/handlers/PresenceUpdate.js deleted file mode 100644 index 2892d44c1..000000000 --- a/src/client/websocket/packets/handlers/PresenceUpdate.js +++ /dev/null @@ -1,68 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class PresenceUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - let user = client.users.get(data.user.id); - const guild = client.guilds.get(data.guild_id); - - // Step 1 - if (!user) { - if (data.user.username) { - user = client.users.add(data.user); - } else { - return; - } - } - - const oldUser = user._update(data.user); - if (!user.equals(oldUser)) { - client.emit(Events.USER_UPDATE, oldUser, user); - } - - if (guild) { - let oldPresence = guild.presences.get(user.id); - if (oldPresence) oldPresence = oldPresence._clone(); - let member = guild.members.get(user.id); - if (!member && data.status !== 'offline') { - member = guild.members.add({ - user, - roles: data.roles, - deaf: false, - mute: false, - }); - client.emit(Events.GUILD_MEMBER_AVAILABLE, member); - } - guild.presences.add(Object.assign(data, { guild })); - if (member && client.listenerCount(Events.PRESENCE_UPDATE)) { - client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); - } - } - } -} - -/** - * Emitted whenever a guild member's presence (e.g. status, activity) is changed. - * @event Client#presenceUpdate - * @param {?Presence} oldPresence The presence before the update, if one at all - * @param {Presence} newPresence The presence after the update - */ - -/** - * Emitted whenever a user's details (e.g. username, avatar) are changed. - * Disabling {@link Client#presenceUpdate} will cause this event to only fire - * on {@link ClientUser} update. - * @event Client#userUpdate - * @param {User} oldUser The user before the update - * @param {User} newUser The user after the update - */ - -/** - * Emitted whenever a member becomes available in a large guild. - * @event Client#guildMemberAvailable - * @param {GuildMember} member The member that became available - */ - -module.exports = PresenceUpdateHandler; diff --git a/src/client/websocket/packets/handlers/Ready.js b/src/client/websocket/packets/handlers/Ready.js deleted file mode 100644 index 5b14c8339..000000000 --- a/src/client/websocket/packets/handlers/Ready.js +++ /dev/null @@ -1,41 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); -let ClientUser; - -class ReadyHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - - client.ws.heartbeat(); - - client.presence.userID = data.user.id; - if (!ClientUser) ClientUser = require('../../../../structures/ClientUser'); - const clientUser = new ClientUser(client, data.user); - client.user = clientUser; - client.readyAt = new Date(); - client.users.set(clientUser.id, clientUser); - - for (const guild of data.guilds) client.guilds.add(guild); - - const t = client.setTimeout(() => { - client.ws.connection.triggerReady(); - }, 1200 * data.guilds.length); - - client.setMaxListeners(data.guilds.length + 10); - - client.once('ready', () => { - client.setMaxListeners(10); - client.clearTimeout(t); - }); - - const ws = this.packetManager.ws; - - ws.sessionID = data.session_id; - ws._trace = data._trace; - client.emit(Events.DEBUG, `READY ${ws._trace.join(' -> ')} ${ws.sessionID}`); - ws.checkIfReady(); - } -} - -module.exports = ReadyHandler; diff --git a/src/client/websocket/packets/handlers/Resumed.js b/src/client/websocket/packets/handlers/Resumed.js deleted file mode 100644 index cd7cab770..000000000 --- a/src/client/websocket/packets/handlers/Resumed.js +++ /dev/null @@ -1,28 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events, Status } = require('../../../../util/Constants'); - -class ResumedHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const ws = client.ws.connection; - - ws._trace = packet.d._trace; - - ws.status = Status.READY; - this.packetManager.handleQueue(); - - const replayed = ws.sequence - ws.closeSequence; - - ws.debug(`RESUMED ${ws._trace.join(' -> ')} | replayed ${replayed} events.`); - client.emit(Events.RESUMED, replayed); - ws.heartbeat(); - } -} - -/** - * Emitted whenever a WebSocket resumes. - * @event Client#resumed - * @param {number} replayed The number of events that were replayed - */ - -module.exports = ResumedHandler; diff --git a/src/client/websocket/packets/handlers/TypingStart.js b/src/client/websocket/packets/handlers/TypingStart.js deleted file mode 100644 index 52a0f6ba8..000000000 --- a/src/client/websocket/packets/handlers/TypingStart.js +++ /dev/null @@ -1,68 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); -const { Events } = require('../../../../util/Constants'); - -class TypingStartHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - const channel = client.channels.get(data.channel_id); - const user = client.users.get(data.user_id); - const timestamp = new Date(data.timestamp * 1000); - - if (channel && user) { - if (channel.type === 'voice') { - client.emit(Events.WARN, `Discord sent a typing packet to voice channel ${channel.id}`); - return; - } - if (channel._typing.has(user.id)) { - const typing = channel._typing.get(user.id); - typing.lastTimestamp = timestamp; - typing.resetTimeout(tooLate(channel, user)); - } else { - channel._typing.set(user.id, new TypingData(client, timestamp, timestamp, tooLate(channel, user))); - client.emit(Events.TYPING_START, channel, user); - } - } - } -} - -class TypingData { - constructor(client, since, lastTimestamp, _timeout) { - this.client = client; - this.since = since; - this.lastTimestamp = lastTimestamp; - this._timeout = _timeout; - } - - resetTimeout(_timeout) { - this.client.clearTimeout(this._timeout); - this._timeout = _timeout; - } - - get elapsedTime() { - return Date.now() - this.since; - } -} - -function tooLate(channel, user) { - return channel.client.setTimeout(() => { - channel.client.emit(Events.TYPING_STOP, channel, user, channel._typing.get(user.id)); - channel._typing.delete(user.id); - }, 6000); -} - -/** - * Emitted whenever a user starts typing in a channel. - * @event Client#typingStart - * @param {Channel} channel The channel the user started typing in - * @param {User} user The user that started typing - */ - -/** - * Emitted whenever a user stops typing in a channel. - * @event Client#typingStop - * @param {Channel} channel The channel the user stopped typing in - * @param {User} user The user that stopped typing - */ - -module.exports = TypingStartHandler; diff --git a/src/client/websocket/packets/handlers/UserUpdate.js b/src/client/websocket/packets/handlers/UserUpdate.js deleted file mode 100644 index bc34f347d..000000000 --- a/src/client/websocket/packets/handlers/UserUpdate.js +++ /dev/null @@ -1,11 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -class UserUpdateHandler extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.actions.UserUpdate.handle(data); - } -} - -module.exports = UserUpdateHandler; diff --git a/src/client/websocket/packets/handlers/VoiceServerUpdate.js b/src/client/websocket/packets/handlers/VoiceServerUpdate.js deleted file mode 100644 index 97885d6cd..000000000 --- a/src/client/websocket/packets/handlers/VoiceServerUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -const AbstractHandler = require('./AbstractHandler'); - -/* -{ - "token": "my_token", - "guild_id": "41771983423143937", - "endpoint": "smart.loyal.discord.gg" -} -*/ - -class VoiceServerUpdate extends AbstractHandler { - handle(packet) { - const client = this.packetManager.client; - const data = packet.d; - client.emit('self.voiceServer', data); - } -} - -module.exports = VoiceServerUpdate; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 2d1bc3c2c..2e2ec97bb 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -6,6 +6,7 @@ const Messages = { TOKEN_INVALID: 'An invalid token was provided.', TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', + WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', WS_CONNECTION_TIMEOUT: 'The connection to the gateway timed out.', WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index a43bb7e92..3b25c1d0a 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -29,7 +29,7 @@ class Shard extends EventEmitter { this.manager = manager; /** - * ID of the shard + * ID of the shard in the manager * @type {number} */ this.id = id; @@ -51,8 +51,10 @@ class Shard extends EventEmitter { * @type {Object} */ this.env = Object.assign({}, process.env, { - SHARD_ID: this.id, - SHARD_COUNT: this.manager.totalShards, + SHARDING_MANAGER: true, + SHARDS: this.id, + TOTAL_SHARD_COUNT: this.manager.totalShards, + DISCORD_TOKEN: this.manager.token, }); /** diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 4aa35a3b2..9a2a746bc 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -49,7 +49,7 @@ class ShardClientUtil { * @readonly */ get id() { - return this.client.options.shardId; + return this.client.options.shards; } /** diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 7cedc37ef..9bfce6fe2 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -27,7 +27,8 @@ class ShardingManager extends EventEmitter { /** * @param {string} file Path to your shard script file * @param {Object} [options] Options for the sharding manager - * @param {number|string} [options.totalShards='auto'] Number of shards to spawn, or "auto" + * @param {string|number[]} [options.totalShards='auto'] Number of total shards of all shard managers or "auto" + * @param {string|number[]} [options.shardList='auto'] List of shards to spawn or "auto" * @param {ShardingManagerMode} [options.mode='process'] Which mode to use for shards * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting * @param {string[]} [options.shardArgs=[]] Arguments to pass to the shard script when spawning @@ -58,16 +59,33 @@ class ShardingManager extends EventEmitter { if (!stats.isFile()) throw new Error('CLIENT_INVALID_OPTION', 'File', 'a file'); /** - * Amount of shards that this manager is going to spawn - * @type {number|string} + * List of shards this sharding manager spawns + * @type {string|number[]} */ - this.totalShards = options.totalShards; + this.shardList = options.shardList || 'auto'; + if (this.shardList !== 'auto') { + if (!Array.isArray(this.shardList)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array.'); + } + this.shardList = [...new Set(this.shardList)]; + if (this.shardList.length < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardList', 'at least 1 ID.'); + if (this.shardList.some(shardID => typeof shardID !== 'number' || isNaN(shardID) || + !Number.isInteger(shardID) || shardID < 0)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of postive integers.'); + } + } + + /** + * Amount of shards that all sharding managers spawn in total + * @type {number} + */ + this.totalShards = options.totalShards || 'auto'; if (this.totalShards !== 'auto') { if (typeof this.totalShards !== 'number' || isNaN(this.totalShards)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); } if (this.totalShards < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); - if (this.totalShards !== Math.floor(this.totalShards)) { + if (!Number.isInteger(this.totalShards)) { throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } } @@ -150,21 +168,31 @@ class ShardingManager extends EventEmitter { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'a number.'); } if (amount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'at least 1.'); - if (amount !== Math.floor(amount)) { + if (!Number.isInteger(amount)) { throw new TypeError('CLIENT_INVALID_OPTION', 'Amount of shards', 'an integer.'); } } // Make sure this many shards haven't already been spawned if (this.shards.size >= amount) throw new Error('SHARDING_ALREADY_SPAWNED', this.shards.size); - this.totalShards = amount; + if (this.shardList === 'auto' || this.totalShards === 'auto' || this.totalShards !== amount) { + this.shardList = [...Array(amount).keys()]; + } + if (this.totalShards === 'auto' || this.totalShards !== amount) { + this.totalShards = amount; + } + + if (this.shardList.some(shardID => shardID >= amount)) { + throw new RangeError('CLIENT_INVALID_OPTION', 'Amount of shards', + 'bigger than the highest shardID in the shardList option.'); + } // Spawn the shards - for (let s = 1; s <= amount; s++) { + for (const shardID of this.shardList) { const promises = []; - const shard = this.createShard(); + const shard = this.createShard(shardID); promises.push(shard.spawn(waitForReady)); - if (delay > 0 && s !== amount) promises.push(Util.delayFor(delay)); + if (delay > 0 && this.shards.size !== this.shardList.length - 1) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 44494be49..6f95aec92 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -189,7 +189,7 @@ class GuildMemberStore extends DataStore { resolve(query || limit ? new Collection() : this); return; } - this.guild.client.ws.send({ + this.guild.shard.send({ op: OPCodes.REQUEST_GUILD_MEMBERS, d: { guild_id: this.guild.id, diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 90d1cb4a2..06924589d 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -11,7 +11,15 @@ class ClientPresence extends Presence { async set(presence) { const packet = await this._parse(presence); this.patch(packet); - this.client.ws.send({ op: OPCodes.STATUS_UPDATE, d: packet }); + if (typeof presence.shardID === 'undefined') { + this.client.ws.broadcast({ op: OPCodes.STATUS_UPDATE, d: packet }); + } else if (Array.isArray(presence.shardID)) { + for (const shardID of presence.shardID) { + this.client.ws.shards[shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } + } else { + this.client.ws.shards[presence.shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + } return this; } diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 9827e16de..1b8d90d98 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -15,14 +15,14 @@ class ClientUser extends Structures.get('User') { */ this.verified = data.verified; - this._typing = new Map(); - /** * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account * @type {?boolean} */ this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + this._typing = new Map(); + if (data.token) this.client.token = data.token; } @@ -39,7 +39,9 @@ class ClientUser extends Structures.get('User') { return this.client.api.users('@me').patch({ data }) .then(newData => { this.client.token = newData.token; - return this.client.actions.UserUpdate.handle(newData).updated; + const { updated } = this.client.actions.UserUpdate.handle(newData); + if (updated) return updated; + return this; }); } @@ -84,6 +86,7 @@ class ClientUser extends Structures.get('User') { * @property {string} [activity.name] Name of the activity * @property {ActivityType|number} [activity.type] Type of the activity * @property {string} [activity.url] Stream url + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on */ /** @@ -112,6 +115,7 @@ class ClientUser extends Structures.get('User') { /** * Sets the status of the client user. * @param {PresenceStatus} status Status to change to + * @param {?number|number[]} [shardID] Shard ID(s) to have the activity set on * @returns {Promise} * @example * // Set the client user's status @@ -119,8 +123,8 @@ class ClientUser extends Structures.get('User') { * .then(console.log) * .catch(console.error); */ - setStatus(status) { - return this.setPresence({ status }); + setStatus(status, shardID) { + return this.setPresence({ status, shardID }); } /** @@ -129,6 +133,7 @@ class ClientUser extends Structures.get('User') { * @type {Object} * @property {string} [url] Twitch stream URL * @property {ActivityType|number} [type] Type of the activity + * @property {?number|number[]} [shardID] Shard Id(s) to have the activity set on */ /** @@ -143,10 +148,10 @@ class ClientUser extends Structures.get('User') { * .catch(console.error); */ setActivity(name, options = {}) { - if (!name) return this.setPresence({ activity: null }); + if (!name) return this.setPresence({ activity: null, shardID: options.shardID }); const activity = Object.assign({}, options, typeof name === 'object' ? name : { name }); - return this.setPresence({ activity }); + return this.setPresence({ activity, shardID: activity.shardID }); } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 493c0df81..ffef8578b 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -74,6 +74,21 @@ class Guild extends Base { this._patch(data); if (!data.channels) this.available = false; } + + /** + * The id of the shard this Guild belongs to. + * @type {number} + */ + this.shardID = data.shardID; + } + + /** + * The Shard this Guild belongs to. + * @type {WebSocketShard} + * @readonly + */ + get shard() { + return this.client.ws.shards[this.shardID]; } /* eslint-disable complexity */ diff --git a/src/util/Constants.js b/src/util/Constants.js index 32413c257..e956c8a44 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -5,8 +5,10 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number} [shardId=0] ID of the shard to run - * @property {number} [shardCount=0] Total number of shards + * @property {number|number[]} [shards=0] ID of the shard to run, or an array of shard IDs + * @property {number} [shardCount=1] Total number of shards that will be spawned by this Client + * @property {number} [totalShardCount=1] The total amount of shards used by all processes of this bot + * (e.g. recommended shard count, shard count of the ShardingManager) * @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel * (-1 or Infinity for unlimited - don't do this without message sweeping, otherwise memory usage will climb * indefinitely) @@ -33,9 +35,9 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {HTTPOptions} [http] HTTP options */ exports.DefaultOptions = { - shardId: 0, - shardCount: 0, - internalSharding: false, + shards: 0, + shardCount: 1, + totalShardCount: 1, messageCacheMaxSize: 200, messageCacheLifetime: 0, messageSweepInterval: 0, @@ -86,10 +88,10 @@ exports.UserAgent = browser ? null : `DiscordBot (${Package.homepage.split('#')[0]}, ${Package.version}) Node.js/${process.version}`; exports.WSCodes = { - 1000: 'Connection gracefully closed', - 4004: 'Tried to identify with an invalid token', - 4010: 'Sharding data provided was invalid', - 4011: 'Shard would be on too many guilds if connected', + 1000: 'WS_CLOSE_REQUESTED', + 4004: 'TOKEN_INVALID', + 4010: 'SHARDING_INVALID', + 4011: 'SHARDING_REQUIRED', }; const AllowedImageFormats = [ @@ -253,6 +255,9 @@ exports.Events = { ERROR: 'error', WARN: 'warn', DEBUG: 'debug', + SHARD_READY: 'shardReady', + INVALIDATED: 'invalidated', + RAW: 'raw', }; /** diff --git a/test/shard.js b/test/shard.js index 5f5f17c49..5c185b5fa 100644 --- a/test/shard.js +++ b/test/shard.js @@ -2,7 +2,7 @@ const Discord = require('../'); const { token } = require('./auth.json'); const client = new Discord.Client({ - shardId: process.argv[2], + shardID: process.argv[2], shardCount: process.argv[3], }); @@ -20,8 +20,8 @@ client.on('message', msg => { process.send(123); client.on('ready', () => { - console.log('Ready', client.options.shardId); - if (client.options.shardId === 0) + console.log('Ready', client.options.shardID); + if (client.options.shardID === 0) setTimeout(() => { console.log('kek dying'); client.destroy(); diff --git a/test/tester1000.js b/test/tester1000.js index 99209c71a..d726188c5 100644 --- a/test/tester1000.js +++ b/test/tester1000.js @@ -4,7 +4,9 @@ const { token, prefix, owner } = require('./auth.js'); // eslint-disable-next-line no-console const log = (...args) => console.log(process.uptime().toFixed(3), ...args); -const client = new Discord.Client(); +const client = new Discord.Client({ + shardCount: 2, +}); client.on('debug', log); client.on('ready', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 2672d1845..ddda0bfef 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -80,17 +80,20 @@ declare module 'discord.js' { export class BaseClient extends EventEmitter { constructor(options?: ClientOptions); - private _intervals: Set; private _timeouts: Set; + private _intervals: Set; + private _immediates: Set; private readonly api: object; private rest: object; public options: ClientOptions; public clearInterval(interval: NodeJS.Timer): void; public clearTimeout(timeout: NodeJS.Timer): void; + public clearImmediate(timeout: NodeJS.Immediate): void; public destroy(): void; public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; + public setImmediate(fn: Function, delay: number, ...args: any[]): NodeJS.Immediate; public toJSON(...props: { [key: string]: boolean | string }[]): object; } @@ -133,31 +136,26 @@ declare module 'discord.js' { export class Client extends BaseClient { constructor(options?: ClientOptions); - private readonly _pingTimestamp: number; private actions: object; - private manager: ClientManager; private voice: object; - private ws: object; private _eval(script: string): any; - private _pong(startTime: number): void; private _validateOptions(options?: ClientOptions): void; public broadcasts: VoiceBroadcast[]; public channels: ChannelStore; public readonly emojis: GuildEmojiStore; public guilds: GuildStore; - public readonly ping: number; - public pings: number[]; - public readyAt: Date; + public readyAt: Date | null; public readonly readyTimestamp: number; public shard: ShardClientUtil; - public readonly status: Status; public token: string; public readonly uptime: number; - public user: ClientUser; + public user: ClientUser | null; public users: UserStore; public readonly voiceConnections: Collection; + public ws: WebSocketManager; public createVoiceBroadcast(): VoiceBroadcast; + public destroy(): void; public fetchApplication(): Promise; public fetchInvite(invite: InviteResolvable): Promise; public fetchVoiceRegions(): Promise>; @@ -171,7 +169,7 @@ declare module 'discord.js' { public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; public on(event: 'debug' | 'warn', listener: (info: string) => void): this; - public on(event: 'disconnect', listener: (event: any) => void): this; + public on(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public on(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public on(event: 'error', listener: (error: Error) => void): this; @@ -189,10 +187,12 @@ declare module 'discord.js' { public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public on(event: 'ready' | 'reconnecting', listener: () => void): this; - public on(event: 'resumed', listener: (replayed: number) => void): this; + public on(event: 'ready', listener: () => void): this; + public on(event: 'reconnecting', listener: (shardID: number) => void): this; + public on(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public on(event: 'shardReady', listener: (shardID: number) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; @@ -203,7 +203,7 @@ declare module 'discord.js' { public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; public once(event: 'debug' | 'warn', listener: (info: string) => void): this; - public once(event: 'disconnect', listener: (event: any) => void): this; + public once(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public once(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public once(event: 'error', listener: (error: Error) => void): this; @@ -221,10 +221,12 @@ declare module 'discord.js' { public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public once(event: 'ready' | 'reconnecting', listener: () => void): this; - public once(event: 'resumed', listener: (replayed: number) => void): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'reconnecting', listener: (shardID: number) => void): this; + public once(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; + public once(event: 'shardReady', listener: (shardID: number) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; @@ -252,18 +254,11 @@ declare module 'discord.js' { public toString(): string; } - class ClientManager { - constructor(client: Client); - public client: Client; - public heartbeatInterval: number; - public readonly status: number; - public connectToWebSocket(token: string, resolve: Function, reject: Function): void; - } - export interface ActivityOptions { name?: string; url?: string; type?: ActivityType | number; + shardID?: number | number[]; } export class ClientUser extends User { @@ -275,7 +270,7 @@ declare module 'discord.js' { public setAFK(afk: boolean): Promise; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; public setPresence(data: PresenceData): Promise; - public setStatus(status: PresenceStatus): Promise; + public setStatus(status: PresenceStatus, shardID?: number | number[]): Promise; public setUsername(username: string): Promise; } @@ -1289,6 +1284,31 @@ declare module 'discord.js' { constructor(id: string, token: string, options?: ClientOptions); } + export class WebSocketManager { + constructor(client: Client); + public readonly client: Client; + public gateway: string | undefined; + public readonly ping: number; + public shards: WebSocketShard[]; + public sessionStartLimit: { total: number; remaining: number; reset_after: number; }; + public status: Status; + public broadcast(packet: any): void; + } + + export class WebSocketShard extends EventEmitter { + constructor(manager: WebSocketManager, id: number, oldShard?: WebSocketShard); + public id: number; + public readonly ping: number; + public pings: number[]; + public status: Status; + public manager: WebSocketManager; + public send(data: object): void; + + public on(event: 'ready', listener: () => void): this; + + public once(event: 'ready', listener: () => void): this; + } + //#endregion //#region Stores @@ -1572,9 +1592,9 @@ declare module 'discord.js' { }; type ClientOptions = { - presence?: PresenceData; - shardId?: number; + shards?: number | number[]; shardCount?: number; + totalShardCount?: number; messageCacheMaxSize?: number; messageCacheLifetime?: number; messageSweepInterval?: number; @@ -1582,7 +1602,9 @@ declare module 'discord.js' { disableEveryone?: boolean; restWsBridgeTimeout?: number; restTimeOffset?: number; - retryLimit?: number, + restSweepInterval?: number; + retryLimit?: number; + presence?: PresenceData; disabledEvents?: WSEventType[]; ws?: WebSocketOptions; http?: HTTPOptions; @@ -1982,7 +2004,8 @@ declare module 'discord.js' { name?: string; type?: ActivityType | number; url?: string; - } + }; + shardID?: number | number[]; }; type PresenceResolvable = Presence | UserResolvable | Snowflake; From be0d1cd6637bca837f687bfd00e07e8bacf52838 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 4 Nov 2018 23:25:54 -0600 Subject: [PATCH 0921/1359] fix: Client#shards not being set properly --- src/client/Client.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/Client.js b/src/client/Client.js index 339e55646..5d5deddf6 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -53,6 +53,10 @@ class Client extends BaseClient { this.options.totalShardCount = this.options.shardCount; } } + if (!this.options.shards && this.options.shardCount) { + this.options.shards = []; + for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); + } this._validateOptions(); @@ -227,6 +231,10 @@ class Client extends BaseClient { this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); this.options.shardCount = res.shards; this.options.totalShardCount = res.shards; + if (!this.options.shards || !this.options.shards.length) { + this.options.shards = []; + for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); + } } this.emit(Events.DEBUG, `Using gateway ${gateway}`); this.ws.connect(gateway); From 7796cb5d0589a40fa719676d4b8b129a3598732e Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Sun, 4 Nov 2018 23:26:40 -0600 Subject: [PATCH 0922/1359] fix: Client#raw emitting twice --- src/client/websocket/WebSocketShard.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1c51be0a9..0058398e4 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -202,8 +202,6 @@ class WebSocketShard extends EventEmitter { return false; } - this.manager.client.emit(Events.RAW, packet, this.id); - switch (packet.t) { case WSEvents.READY: this.sessionID = packet.d.session_id; @@ -272,7 +270,7 @@ class WebSocketShard extends EventEmitter { let packet; try { packet = WebSocket.unpack(this.inflate.result); - this.manager.client.emit(Events.RAW, packet); + this.manager.client.emit(Events.RAW, packet, this.id); } catch (err) { this.manager.client.emit(Events.ERROR, err); return; From 905f1c326259b841e47f7bccdad1fe8c79e36e95 Mon Sep 17 00:00:00 2001 From: Souji Date: Mon, 5 Nov 2018 16:46:47 +0000 Subject: [PATCH 0923/1359] docs: add missing docstring for Client#userUpdate (#2930) * docs: add missing docstring for Client#userUpdate * docs: indentn't * docs: indentn't 2 --- src/client/actions/UserUpdate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index 60adc1746..b9c097488 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -9,6 +9,12 @@ class UserUpdateAction extends Action { const oldUser = newUser._update(data.user); if (!oldUser.equals(newUser)) { + /** + * Emitted whenever a user's details (e.g. username) are changed. + * @event Client#userUpdate + * @param {User} oldUser The user before the update + * @param {User} newUser The user after the update + */ client.emit(Events.USER_UPDATE, oldUser, newUser); return { old: oldUser, From b59c75e402b91d8a7ecec6f0b67180598e8ccdb5 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Mon, 5 Nov 2018 13:29:07 -0600 Subject: [PATCH 0924/1359] docs: add missing docstring for Client#guildDelete --- src/client/actions/GuildDelete.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 1bf237bd9..27ea1b001 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -19,6 +19,12 @@ class GuildDeleteAction extends Action { if (guild.available && data.unavailable) { // Guild is unavailable guild.available = false; + + /** + * Emitted whenever a guild becomes unavailable, likely due to a server outage. + * @event Client#guildUnavailable + * @param {Guild} guild The guild that has become unavailable + */ client.emit(Events.GUILD_UNAVAILABLE, guild); // Stops the GuildDelete packet thinking a guild was actually deleted, @@ -34,7 +40,14 @@ class GuildDeleteAction extends Action { // Delete guild client.guilds.remove(guild.id); guild.deleted = true; + + /** + * Emitted whenever a guild kicks the client or the guild is deleted/left. + * @event Client#guildDelete + * @param {Guild} guild The guild that was deleted + */ client.emit(Events.GUILD_DELETE, guild); + this.deleted.set(guild.id, guild); this.scheduleForDeletion(guild.id); } else { @@ -49,10 +62,4 @@ class GuildDeleteAction extends Action { } } -/** - * Emitted whenever a guild becomes unavailable, likely due to a server outage. - * @event Client#guildUnavailable - * @param {Guild} guild The guild that has become unavailable - */ - module.exports = GuildDeleteAction; From 08002d0576c7dae89a625a8f11944ab81c4ff4c9 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Mon, 5 Nov 2018 18:24:50 -0600 Subject: [PATCH 0925/1359] fix: Client#userUpdate receiving wrong packet --- src/client/actions/PresenceUpdate.js | 2 +- src/client/actions/UserUpdate.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 6da03caf2..53b18c80a 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -8,7 +8,7 @@ class PresenceUpdateAction extends Action { if (!cached) return; if (data.user && data.user.username) { - if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data); + if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); } const guild = this.client.guilds.get(data.guild_id); diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index b9c097488..3ff407901 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -5,8 +5,8 @@ class UserUpdateAction extends Action { handle(data) { const client = this.client; - const newUser = client.users.get(data.user.id); - const oldUser = newUser._update(data.user); + const newUser = client.users.get(data.id); + const oldUser = newUser._update(data); if (!oldUser.equals(newUser)) { /** From 6b886b0abad86a314cbdf76ab85872b46ed2ef63 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Tue, 6 Nov 2018 11:23:17 -0600 Subject: [PATCH 0926/1359] typings: add Guild#shard and Guild#shardID --- typings/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index ddda0bfef..30ccf253b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -434,6 +434,8 @@ declare module 'discord.js' { public presences: PresenceStore; public region: string; public roles: RoleStore; + public shard: WebSocketShard; + public shardID: number; public splash: string; public readonly systemChannel: TextChannel; public systemChannelID: Snowflake; From 3418b5a1a2e2e424369da42f0a04dc3523f3d933 Mon Sep 17 00:00:00 2001 From: Lewdcario Date: Tue, 6 Nov 2018 14:59:02 -0600 Subject: [PATCH 0927/1359] docs: restore Client#error docs that went missing --- src/client/websocket/WebSocketShard.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 0058398e4..e9c01e0b0 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -303,6 +303,12 @@ class WebSocketShard extends EventEmitter { return; } this.emit(Events.INVALIDATED); + + /** + * Emitted whenever the client's WebSocket encounters a connection error. + * @event Client#error + * @param {Error} error The encountered error + */ this.manager.client.emit(Events.ERROR, error); } From 2d68e837e5003baf5ac86e79b7f7d0e0bcd4c26a Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 15 Nov 2018 21:38:02 +0000 Subject: [PATCH 0928/1359] voice: fix receiver null on immediate voiceStateUpdate --- src/client/voice/VoiceConnection.js | 3 +-- src/client/voice/networking/VoiceUDPClient.js | 2 ++ src/client/voice/receiver/Receiver.js | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 004cf2019..6a4d4cdaa 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -113,7 +113,7 @@ class VoiceConnection extends EventEmitter { * The voice receiver of this connection * @type {VoiceReceiver} */ - this.receiver = null; + this.receiver = new VoiceReceiver(this); this.authenticate(); } @@ -418,7 +418,6 @@ class VoiceConnection extends EventEmitter { Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; clearTimeout(this.connectTimeout); - this.receiver = new VoiceReceiver(this); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, * the connection will already be ready. diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index f8668be43..fe34cab33 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -108,6 +108,8 @@ class VoiceConnectionUDPClient extends EventEmitter { }, }, }); + + socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer)); }); const blankMessage = Buffer.alloc(70); diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index 8b626be79..1174f70bf 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -21,7 +21,6 @@ class VoiceReceiver extends EventEmitter { * @param {Error|string} error The error or message to debug */ this.packets.on('error', err => this.emit('debug', err)); - this.connection.sockets.udp.socket.on('message', buffer => this.packets.push(buffer)); } /** From 54aff3191e3576dce519634fdbf31bbaabfd907c Mon Sep 17 00:00:00 2001 From: Kyra Date: Fri, 16 Nov 2018 16:55:15 +0100 Subject: [PATCH 0929/1359] feat(Constants): add error code 50020 (#2953) * feat(Constants): Add error code 50020 Which is throw when using the vanity-url endpoint: https://github.com/discordapp/discord-api-docs/pull/748/ * docs: Document the new code --- src/util/Constants.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index e956c8a44..01ff57d6f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -458,6 +458,7 @@ exports.Colors = { * * NOTE_TOO_LONG * * INVALID_BULK_DELETE_QUANTITY * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL + * * INVALID_OR_TAKEN_INVITE_CODE * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE * * BULK_DELETE_MESSAGE_TOO_OLD * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT @@ -505,6 +506,7 @@ exports.APIErrors = { NOTE_TOO_LONG: 50015, INVALID_BULK_DELETE_QUANTITY: 50016, CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, + INVALID_OR_TAKEN_INVITE_CODE: 50020, CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, BULK_DELETE_MESSAGE_TOO_OLD: 50034, INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, From e793338d7403c98589e92943fee28068c18d481c Mon Sep 17 00:00:00 2001 From: Yukine Date: Sat, 17 Nov 2018 05:46:02 +0100 Subject: [PATCH 0930/1359] fix: ShardClientUtil#count and ShardClientUtil#id typedef (#2956) --- src/sharding/ShardClientUtil.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 9a2a746bc..201568ca1 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -44,8 +44,8 @@ class ShardClientUtil { } /** - * ID of this shard - * @type {number} + * Shard ID or array of shard IDs of this client + * @type {number|number[]} * @readonly */ get id() { @@ -58,7 +58,7 @@ class ShardClientUtil { * @readonly */ get count() { - return this.client.options.shardCount; + return this.client.options.totalShardCount; } /** From 81ff5075e4e353c6f5e036f1ead7b85a5aec430e Mon Sep 17 00:00:00 2001 From: Charalampos Fanoulis <38255093+cfanoulis@users.noreply.github.com> Date: Sat, 17 Nov 2018 16:41:12 +0200 Subject: [PATCH 0931/1359] chore: remove user account checkbox from bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c497cb4fa..d78410ccb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,11 +28,11 @@ You won't receive any basic help here. - Priority this issue should have – please be realistic and elaborate if possible: -- [ ] I found this issue while running code on a __user account__ + - [ ] I have also tested the issue on latest master, commit hash: From d92ee2ff999954899c891c093f5f107dd686b64f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 17 Nov 2018 15:43:04 +0100 Subject: [PATCH 0932/1359] feat(GuildChannel): allow to set all options when cloning (#2937) --- src/structures/GuildChannel.js | 24 +++++++++++++----------- typings/index.d.ts | 13 +------------ 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 3567ea4f9..d75678b7d 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -447,35 +447,37 @@ class GuildChannel extends Channel { return invites; } + /* eslint-disable max-len */ /** * Clones this channel. * @param {Object} [options] The options - * @param {string} [options.name=this.name] Optional name for the new channel, otherwise it has the name - * of this channel - * @param {boolean} [options.withPermissions=true] Whether to clone the channel with this channel's - * permission overwrites - * @param {boolean} [options.withTopic=true] Whether to clone the channel with this channel's topic + * @param {string} [options.name=this.name] Name of the new channel + * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites=this.permissionOverwrites] + * Permission overwrites of the new channel + * @param {string} [options.type=this.type] Type of the new channel + * @param {string} [options.topic=this.topic] Topic of the new channel (only text) * @param {boolean} [options.nsfw=this.nsfw] Whether the new channel is nsfw (only text) * @param {number} [options.bitrate=this.bitrate] Bitrate of the new channel in bits (only voice) * @param {number} [options.userLimit=this.userLimit] Maximum amount of users allowed in the new channel (only voice) - * @param {ChannelResolvable} [options.parent=this.parent] The parent of the new channel + * @param {number} [options.rateLimitPerUser=ThisType.rateLimitPerUser] Ratelimit per user for the new channel (only text) + * @param {ChannelResolvable} [options.parent=this.parent] Parent of the new channel * @param {string} [options.reason] Reason for cloning this channel * @returns {Promise} */ + /* eslint-enable max-len */ clone(options = {}) { - if (typeof options.withPermissions === 'undefined') options.withPermissions = true; - if (typeof options.withTopic === 'undefined') options.withTopic = true; Util.mergeDefault({ name: this.name, - permissionOverwrites: options.withPermissions ? this.permissionOverwrites : [], - topic: options.withTopic ? this.topic : undefined, + permissionOverwrites: this.permissionOverwrites, + topic: this.topic, + type: this.type, nsfw: this.nsfw, parent: this.parent, bitrate: this.bitrate, userLimit: this.userLimit, + rateLimitPerUser: this.rateLimitPerUser, reason: null, }, options); - options.type = this.type; return this.guild.channels.create(options.name, options); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 30ccf253b..faed57ef2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,7 +523,7 @@ declare module 'discord.js' { public readonly permissionsLocked: boolean; public readonly position: number; public rawPosition: number; - public clone(options?: GuildChannelCloneOptions): Promise; + public clone(options?: GuildCreateChannelOptions): Promise; public createInvite(options?: InviteOptions): Promise; public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; public edit(data: ChannelData, reason?: string): Promise; @@ -1772,17 +1772,6 @@ declare module 'discord.js' { MESSAGE?: string; }; - type GuildChannelCloneOptions = { - bitrate?: number; - name?: string; - nsfw?: boolean; - parent?: ChannelResolvable; - reason?: string; - userLimit?: number; - withPermissions?: boolean; - withTopic?: boolean; - }; - type GuildChannelResolvable = Snowflake | GuildChannel; type GuildCreateChannelOptions = { From 377ecd73ea1c9c1f5dba47768d97a105075d6bac Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 20 Nov 2018 16:42:27 +0100 Subject: [PATCH 0933/1359] docs(GuildChannel): fix doc string for clone method --- src/structures/GuildChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index d75678b7d..daf20a52d 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -464,7 +464,6 @@ class GuildChannel extends Channel { * @param {string} [options.reason] Reason for cloning this channel * @returns {Promise} */ - /* eslint-enable max-len */ clone(options = {}) { Util.mergeDefault({ name: this.name, @@ -480,6 +479,7 @@ class GuildChannel extends Channel { }, options); return this.guild.channels.create(options.name, options); } + /* eslint-enable max-len */ /** * Checks if this channel has the same type, topic, position, name, overwrites and ID as another channel. From 9085138f0d0845cb52442c62e4dbe67bd1d284aa Mon Sep 17 00:00:00 2001 From: Frangu Vlad Date: Wed, 21 Nov 2018 21:42:37 +0200 Subject: [PATCH 0934/1359] fix: Sharding Issues & Cleanup (#2952) * fix: Sharding causing constant heartbeat / identify spam * misc: Remove wait param in connect * misc: Wait 2.5 seconds before sending identify again if session is resumable * misc: Remove useless destroy call * nit: Capitalization * fix: Identify on HELLO not connectionOpen * misc: Add different intervals for identify after invalid session - 2500 if we couldn't resume in time - 5000 if we didn't have a session ID (per the docs on identify, that a client can only connect every 5 seconds) - Otherwise, just identify again * misc: Only clear heartbeat if shard is fully dead Reconnect clears it otherwise * fix: Accessing .length on a Collection --- src/client/websocket/WebSocketManager.js | 27 +++++++------- src/client/websocket/WebSocketShard.js | 47 +++++++++++++++++++----- src/sharding/ShardingManager.js | 2 +- src/structures/ClientPresence.js | 4 +- src/structures/Guild.js | 2 +- typings/index.d.ts | 2 +- 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 4e4eec1ee..891da3f45 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,3 +1,4 @@ +const Collection = require('../../util/Collection'); const WebSocketShard = require('./WebSocketShard'); const { Events, Status, WSEvents } = require('../../util/Constants'); const PacketHandlers = require('./handlers'); @@ -32,9 +33,9 @@ class WebSocketManager { /** * An array of shards spawned by this WebSocketManager. - * @type {WebSocketShard[]} + * @type {Collection} */ - this.shards = []; + this.shards = new Collection(); /** * An array of queued shards to be spawned by this WebSocketManager. @@ -80,7 +81,7 @@ class WebSocketManager { */ get ping() { const sum = this.shards.reduce((a, b) => a + b.ping, 0); - return sum / this.shards.length; + return sum / this.shards.size; } /** @@ -133,8 +134,8 @@ class WebSocketManager { if (typeof item === 'string' && !isNaN(item)) item = Number(item); if (typeof item === 'number') { - const shard = new WebSocketShard(this, item, this.shards[item]); - this.shards[item] = shard; + const shard = new WebSocketShard(this, item, this.shards.get(item)); + this.shards.set(item, shard); shard.once(Events.READY, () => { this.spawning = false; this.client.setTimeout(() => this._handleSessionLimit(shard), 5000); @@ -161,8 +162,8 @@ class WebSocketManager { this.spawn(this.client.options.shards); } else if (Array.isArray(this.client.options.shards)) { this.debug(`Spawning ${this.client.options.shards.length} shards`); - for (let i = 0; i < this.client.options.shards.length; i++) { - this.spawn(this.client.options.shards[i]); + for (const shard of this.client.options.shards) { + this.spawn(shard); } } else { this.debug(`Spawning ${this.client.options.shardCount} shards`); @@ -190,11 +191,11 @@ class WebSocketManager { if (this.packetQueue.length) { const item = this.packetQueue.shift(); this.client.setImmediate(() => { - this.handlePacket(item.packet, this.shards[item.shardID]); + this.handlePacket(item.packet, this.shards.get(item.shardID)); }); } - if (packet && PacketHandlers[packet.t]) { + if (packet && !this.client.options.disabledEvents.includes(packet.t) && PacketHandlers[packet.t]) { PacketHandlers[packet.t](this.client, packet, shard); } @@ -207,7 +208,7 @@ class WebSocketManager { * @private */ checkReady() { - if (this.shards.filter(s => s).length !== this.client.options.shardCount || + if (this.shards.size !== this.client.options.shardCount || this.shards.some(s => s && s.status !== Status.READY)) { return false; } @@ -257,8 +258,7 @@ class WebSocketManager { * @param {*} packet The packet to send */ broadcast(packet) { - for (const shard of this.shards) { - if (!shard) continue; + for (const shard of this.shards.values()) { shard.send(packet); } } @@ -273,8 +273,7 @@ class WebSocketManager { // Lock calls to spawn this.spawning = true; - for (const shard of this.shards) { - if (!shard) continue; + for (const shard of this.shards.values()) { shard.destroy(); } } diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index e9c01e0b0..7aec37e25 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,6 +1,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); +const Util = require('../../util/Util'); let zlib; try { zlib = require('zlib-sync'); @@ -107,6 +108,12 @@ class WebSocketShard extends EventEmitter { */ this.inflate = null; + /** + * Whether or not the WebSocket is expected to be closed + * @type {boolean} + */ + this.expectingClose = false; + this.connect(); } @@ -143,6 +150,7 @@ class WebSocketShard extends EventEmitter { this.heartbeatInterval = null; } else { this.debug(`Setting a heartbeat interval for ${time}ms`); + if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = this.manager.client.setInterval(() => this.heartbeat(), time); } return; @@ -193,7 +201,7 @@ class WebSocketShard extends EventEmitter { /** * Called whenever a packet is received * @param {Object} packet Packet received - * @returns {boolean} + * @returns {any} * @private */ onPacket(packet) { @@ -229,10 +237,18 @@ class WebSocketShard extends EventEmitter { case OPCodes.RECONNECT: return this.reconnect(); case OPCodes.INVALID_SESSION: - if (!packet.d) this.sessionID = null; this.sequence = -1; this.debug('Session invalidated'); - return this.reconnect(Events.INVALIDATED); + // If the session isn't resumable + if (!packet.d) { + // If we had a session ID before + if (this.sessionID) { + this.sessionID = null; + return this.identify(2500); + } + return this.identify(5000); + } + return this.identify(); case OPCodes.HEARTBEAT_ACK: return this.ackHeartbeat(); case OPCodes.HEARTBEAT: @@ -275,7 +291,7 @@ class WebSocketShard extends EventEmitter { this.manager.client.emit(Events.ERROR, err); return; } - if (packet.t === 'READY') { + if (packet.t === WSEvents.READY) { /** * Emitted when a shard becomes ready * @event WebSocketShard#ready @@ -320,6 +336,7 @@ class WebSocketShard extends EventEmitter { /** * Called whenever a connection to the gateway is closed. * @param {CloseEvent} event Close event that was received + * @returns {void} * @private */ onClose(event) { @@ -333,19 +350,22 @@ class WebSocketShard extends EventEmitter { * @param {number} shardID The shard that disconnected */ this.manager.client.emit(Events.DISCONNECT, event, this.id); - this.debug(WSCodes[event.code]); + this.heartbeat(-1); return; } - this.reconnect(Events.INVALIDATED); + this.expectingClose = false; + this.reconnect(Events.INVALIDATED, 5100); } /** * Identifies the client on a connection. + * @param {?number} [wait=0] Amount of time to wait before identifying * @returns {void} * @private */ - identify() { + identify(wait = 0) { + if (wait) return this.manager.client.setTimeout(this.identify.bind(this), wait); return this.sessionID ? this.identifyResume() : this.identifyNew(); } @@ -427,7 +447,7 @@ class WebSocketShard extends EventEmitter { if (this.ratelimit.remaining === 0) return; if (this.ratelimit.queue.length === 0) return; if (this.ratelimit.remaining === this.ratelimit.total) { - this.ratelimit.resetTimer = this.manager.client.setTimeout(() => { + this.ratelimit.timer = this.manager.client.setTimeout(() => { this.ratelimit.remaining = this.ratelimit.total; this.processQueue(); }, this.ratelimit.time); @@ -443,10 +463,11 @@ class WebSocketShard extends EventEmitter { /** * Triggers a shard reconnect. * @param {?string} [event] The event for the shard to emit - * @returns {void} + * @param {?number} [reconnectIn] Time to wait before reconnecting + * @returns {Promise} * @private */ - reconnect(event) { + async reconnect(event, reconnectIn) { this.heartbeat(-1); this.status = Status.RECONNECTING; @@ -457,6 +478,8 @@ class WebSocketShard extends EventEmitter { this.manager.client.emit(Events.RECONNECTING, this.id); if (event === Events.INVALIDATED) this.emit(event); + this.debug(reconnectIn ? `Reconnecting in ${reconnectIn}ms` : 'Reconnecting now'); + if (reconnectIn) await Util.delayFor(reconnectIn); this.manager.spawn(this.id); } @@ -472,6 +495,10 @@ class WebSocketShard extends EventEmitter { this.ws = null; this.status = Status.DISCONNECTED; this.ratelimit.remaining = this.ratelimit.total; + if (this.ratelimit.timer) { + this.manager.client.clearTimeout(this.ratelimit.timer); + this.ratelimit.timer = null; + } } } module.exports = WebSocketShard; diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 9bfce6fe2..b71962290 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -27,7 +27,7 @@ class ShardingManager extends EventEmitter { /** * @param {string} file Path to your shard script file * @param {Object} [options] Options for the sharding manager - * @param {string|number[]} [options.totalShards='auto'] Number of total shards of all shard managers or "auto" + * @param {string|number} [options.totalShards='auto'] Number of total shards of all shard managers or "auto" * @param {string|number[]} [options.shardList='auto'] List of shards to spawn or "auto" * @param {ShardingManagerMode} [options.mode='process'] Which mode to use for shards * @param {boolean} [options.respawn=true] Whether shards should automatically respawn upon exiting diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 06924589d..68dda64c5 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -15,10 +15,10 @@ class ClientPresence extends Presence { this.client.ws.broadcast({ op: OPCodes.STATUS_UPDATE, d: packet }); } else if (Array.isArray(presence.shardID)) { for (const shardID of presence.shardID) { - this.client.ws.shards[shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + this.client.ws.shards.get(shardID).send({ op: OPCodes.STATUS_UPDATE, d: packet }); } } else { - this.client.ws.shards[presence.shardID].send({ op: OPCodes.STATUS_UPDATE, d: packet }); + this.client.ws.shards.get(presence.shardID).send({ op: OPCodes.STATUS_UPDATE, d: packet }); } return this; } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index ffef8578b..a194ea15a 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -88,7 +88,7 @@ class Guild extends Base { * @readonly */ get shard() { - return this.client.ws.shards[this.shardID]; + return this.client.ws.shards.get(this.shardID); } /* eslint-disable complexity */ diff --git a/typings/index.d.ts b/typings/index.d.ts index faed57ef2..71366d56d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1291,7 +1291,7 @@ declare module 'discord.js' { public readonly client: Client; public gateway: string | undefined; public readonly ping: number; - public shards: WebSocketShard[]; + public shards: Collection; public sessionStartLimit: { total: number; remaining: number; reset_after: number; }; public status: Status; public broadcast(packet: any): void; From a8b47a7a6cfa7a823dbb47f250fa5447f4c5320f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 23 Nov 2018 19:46:11 +0100 Subject: [PATCH 0935/1359] feat(GuildChannelStore): add support for create to accept a position --- src/stores/GuildChannelStore.js | 15 ++++++++++++++- typings/index.d.ts | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index e67bcc1c3..d82ddc054 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -33,6 +33,7 @@ class GuildChannelStore extends DataStore { * @param {ChannelResolvable} [options.parent] Parent of the new channel * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites] * Permission overwrites of the new channel + * @param {number} [options.position] Position of the new channel * @param {number} [options.rateLimitPerUser] The ratelimit per user for the channel * @param {string} [options.reason] Reason for creating the channel * @returns {Promise} @@ -54,7 +55,18 @@ class GuildChannelStore extends DataStore { * }) */ async create(name, options = {}) { - let { type, topic, nsfw, bitrate, userLimit, parent, permissionOverwrites, rateLimitPerUser, reason } = options; + let { + type, + topic, + nsfw, + bitrate, + userLimit, + parent, + permissionOverwrites, + position, + rateLimitPerUser, + reason, + } = options; if (parent) parent = this.client.channels.resolveID(parent); if (permissionOverwrites) { permissionOverwrites = permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); @@ -69,6 +81,7 @@ class GuildChannelStore extends DataStore { bitrate, user_limit: userLimit, parent_id: parent, + position, permission_overwrites: permissionOverwrites, rate_limit_per_user: rateLimitPerUser, }, diff --git a/typings/index.d.ts b/typings/index.d.ts index 71366d56d..ea8d55e7f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1782,6 +1782,7 @@ declare module 'discord.js' { parent?: ChannelResolvable; permissionOverwrites?: OverwriteResolvable[] | Collection; rateLimitPerUser?: number; + position?: number; reason?: string }; From 1d1b3f25e1ede5207212f8aafe985b423bf9c464 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Tue, 27 Nov 2018 15:12:25 -0500 Subject: [PATCH 0936/1359] docs: add documentation for Client#channelCreate (#2967) --- src/client/actions/ChannelCreate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 09e74f0f3..b50d42aae 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -13,4 +13,10 @@ class ChannelCreateAction extends Action { } } +/** + * Emitted whenever a channel is created. + * @event Client#channelCreate + * @param {GroupDMChannel|GuildChannel} channel The channel that was created + */ + module.exports = ChannelCreateAction; From fd21bbb7bfbfe0b51111653e140248145525d95f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 27 Nov 2018 21:28:36 +0100 Subject: [PATCH 0937/1359] docs: move event docstrings to the emitting line of code --- src/client/actions/ChannelCreate.js | 11 +++++------ src/client/actions/ChannelDelete.js | 11 +++++------ src/client/actions/GuildBanRemove.js | 8 +++++++- src/client/actions/GuildEmojiCreate.js | 11 +++++------ src/client/actions/GuildEmojiDelete.js | 11 +++++------ src/client/actions/GuildEmojiUpdate.js | 13 ++++++------- src/client/actions/GuildIntegrationsUpdate.js | 10 +++++----- src/client/actions/GuildMemberRemove.js | 10 +++++----- src/client/actions/GuildRoleCreate.js | 10 +++++----- src/client/actions/GuildRoleDelete.js | 10 +++++----- src/client/actions/GuildRoleUpdate.js | 12 ++++++------ src/client/actions/GuildUpdate.js | 12 ++++++------ src/client/actions/MessageCreate.js | 10 +++++----- src/client/actions/MessageDelete.js | 10 +++++----- src/client/actions/MessageDeleteBulk.js | 10 +++++----- src/client/actions/MessageReactionAdd.js | 7 ------- src/client/actions/MessageReactionRemove.js | 12 ++++++------ src/client/actions/PresenceUpdate.js | 6 ++++++ src/client/actions/VoiceStateUpdate.js | 12 ++++++------ src/client/actions/WebhooksUpdate.js | 10 +++++----- .../websocket/handlers/MESSAGE_REACTION_ADD.js | 6 ++++++ 21 files changed, 109 insertions(+), 103 deletions(-) diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index b50d42aae..57b4e6dfd 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -7,16 +7,15 @@ class ChannelCreateAction extends Action { const existing = client.channels.has(data.id); const channel = client.channels.add(data); if (!existing && channel) { + /** + * Emitted whenever a channel is created. + * @event Client#channelCreate + * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created + */ client.emit(Events.CHANNEL_CREATE, channel); } return { channel }; } } -/** - * Emitted whenever a channel is created. - * @event Client#channelCreate - * @param {GroupDMChannel|GuildChannel} channel The channel that was created - */ - module.exports = ChannelCreateAction; diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index f183bbc78..7f182e60c 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -14,6 +14,11 @@ class ChannelDeleteAction extends Action { if (channel) { client.channels.remove(channel.id); channel.deleted = true; + /** + * Emitted whenever a channel is deleted. + * @event Client#channelDelete + * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was deleted + */ client.emit(Events.CHANNEL_DELETE, channel); } @@ -21,10 +26,4 @@ class ChannelDeleteAction extends Action { } } -/** - * Emitted whenever a channel is deleted. - * @event Client#channelDelete - * @param {GroupDMChannel|GuildChannel} channel The channel that was deleted - */ - module.exports = ChannelDeleteAction; diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 782b5fe2a..7d19c75ba 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -6,7 +6,13 @@ class GuildBanRemove extends Action { const client = this.client; const guild = client.guilds.get(data.guild_id); const user = client.users.add(data.user); - if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user); + /** + * Emitted whenever a member is unbanned from a guild. + * @event Client#guildBanRemove + * @param {Guild} guild The guild that the unban occurred in + * @param {User} user The user that was unbanned + */ + if (guild && user) client.emit(Events.GUILD_BAN_REMOVEGUILD_BAN_REMOVE, guild, user); } } diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index 7fc955a0f..121e89ab9 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -4,15 +4,14 @@ const { Events } = require('../../util/Constants'); class GuildEmojiCreateAction extends Action { handle(guild, createdEmoji) { const emoji = guild.emojis.add(createdEmoji); + /** + * Emitted whenever a custom emoji is created in a guild. + * @event Client#emojiCreate + * @param {GuildEmoji} emoji The emoji that was created + */ this.client.emit(Events.GUILD_EMOJI_CREATE, emoji); return { emoji }; } } -/** - * Emitted whenever a custom emoji is created in a guild. - * @event Client#emojiCreate - * @param {GuildEmoji} emoji The emoji that was created - */ - module.exports = GuildEmojiCreateAction; diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index 82df6ec8b..7b94ff41c 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -5,15 +5,14 @@ class GuildEmojiDeleteAction extends Action { handle(emoji) { emoji.guild.emojis.remove(emoji.id); emoji.deleted = true; + /** + * Emitted whenever a custom emoji is deleted in a guild. + * @event Client#emojiDelete + * @param {GuildEmoji} emoji The emoji that was deleted + */ this.client.emit(Events.GUILD_EMOJI_DELETE, emoji); return { emoji }; } } -/** - * Emitted whenever a custom emoji is deleted in a guild. - * @event Client#emojiDelete - * @param {GuildEmoji} emoji The emoji that was deleted - */ - module.exports = GuildEmojiDeleteAction; diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js index e6accf2c5..34e235a0a 100644 --- a/src/client/actions/GuildEmojiUpdate.js +++ b/src/client/actions/GuildEmojiUpdate.js @@ -4,16 +4,15 @@ const { Events } = require('../../util/Constants'); class GuildEmojiUpdateAction extends Action { handle(current, data) { const old = current._update(data); + /** + * Emitted whenever a custom emoji is updated in a guild. + * @event Client#emojiUpdate + * @param {GuildEmoji} oldEmoji The old emoji + * @param {GuildEmoji} newEmoji The new emoji + */ this.client.emit(Events.GUILD_EMOJI_UPDATE, old, current); return { emoji: current }; } } -/** - * Emitted whenever a custom emoji is updated in a guild. - * @event Client#emojiUpdate - * @param {GuildEmoji} oldEmoji The old emoji - * @param {GuildEmoji} newEmoji The new emoji - */ - module.exports = GuildEmojiUpdateAction; diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js index e9c3bdbf4..8bb6eabf3 100644 --- a/src/client/actions/GuildIntegrationsUpdate.js +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -5,14 +5,14 @@ class GuildIntegrationsUpdate extends Action { handle(data) { const client = this.client; const guild = client.guilds.get(data.guild_id); + /** + * Emitted whenever a guild integration is updated + * @event Client#guildIntegrationsUpdate + * @param {Guild} guild The guild whose integrations were updated + */ if (guild) client.emit(Events.GUILD_INTEGRATIONS_UPDATE, guild); } } module.exports = GuildIntegrationsUpdate; -/** - * Emitted whenever a guild integration is updated - * @event Client#guildIntegrationsUpdate - * @param {Guild} guild The guild whose integrations were updated - */ diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index febeb73a0..62373e486 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -13,6 +13,11 @@ class GuildMemberRemoveAction extends Action { guild.voiceStates.delete(member.id); member.deleted = true; guild.members.remove(member.id); + /** + * Emitted whenever a member leaves a guild, or is kicked. + * @event Client#guildMemberRemove + * @param {GuildMember} member The member that has left/been kicked from the guild + */ if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } } @@ -20,10 +25,5 @@ class GuildMemberRemoveAction extends Action { } } -/** - * Emitted whenever a member leaves a guild, or is kicked. - * @event Client#guildMemberRemove - * @param {GuildMember} member The member that has left/been kicked from the guild - */ module.exports = GuildMemberRemoveAction; diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index b4930399d..e4657213b 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -9,16 +9,16 @@ class GuildRoleCreate extends Action { if (guild) { const already = guild.roles.has(data.role.id); role = guild.roles.add(data.role); + /** + * Emitted whenever a role is created. + * @event Client#roleCreate + * @param {Role} role The role that was created + */ if (!already) client.emit(Events.GUILD_ROLE_CREATE, role); } return { role }; } } -/** - * Emitted whenever a role is created. - * @event Client#roleCreate - * @param {Role} role The role that was created - */ module.exports = GuildRoleCreate; diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js index ce6bcaedf..0cecc8efe 100644 --- a/src/client/actions/GuildRoleDelete.js +++ b/src/client/actions/GuildRoleDelete.js @@ -12,6 +12,11 @@ class GuildRoleDeleteAction extends Action { if (role) { guild.roles.remove(data.role_id); role.deleted = true; + /** + * Emitted whenever a guild role is deleted. + * @event Client#roleDelete + * @param {Role} role The role that was deleted + */ client.emit(Events.GUILD_ROLE_DELETE, role); } } @@ -20,10 +25,5 @@ class GuildRoleDeleteAction extends Action { } } -/** - * Emitted whenever a guild role is deleted. - * @event Client#roleDelete - * @param {Role} role The role that was deleted - */ module.exports = GuildRoleDeleteAction; diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js index 2437264fa..c2e082c70 100644 --- a/src/client/actions/GuildRoleUpdate.js +++ b/src/client/actions/GuildRoleUpdate.js @@ -12,6 +12,12 @@ class GuildRoleUpdateAction extends Action { const role = guild.roles.get(data.role.id); if (role) { old = role._update(data.role); + /** + * Emitted whenever a guild role is updated. + * @event Client#roleUpdate + * @param {Role} oldRole The role before the update + * @param {Role} newRole The role after the update + */ client.emit(Events.GUILD_ROLE_UPDATE, old, role); } @@ -28,11 +34,5 @@ class GuildRoleUpdateAction extends Action { } } -/** - * Emitted whenever a guild role is updated. - * @event Client#roleUpdate - * @param {Role} oldRole The role before the update - * @param {Role} newRole The role after the update - */ module.exports = GuildRoleUpdateAction; diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js index b828bb103..ac592d277 100644 --- a/src/client/actions/GuildUpdate.js +++ b/src/client/actions/GuildUpdate.js @@ -8,6 +8,12 @@ class GuildUpdateAction extends Action { const guild = client.guilds.get(data.id); if (guild) { const old = guild._update(data); + /** + * Emitted whenever a guild is updated - e.g. name change. + * @event Client#guildUpdate + * @param {Guild} oldGuild The guild before the update + * @param {Guild} newGuild The guild after the update + */ client.emit(Events.GUILD_UPDATE, old, guild); return { old, @@ -22,11 +28,5 @@ class GuildUpdateAction extends Action { } } -/** - * Emitted whenever a guild is updated - e.g. name change. - * @event Client#guildUpdate - * @param {Guild} oldGuild The guild before the update - * @param {Guild} newGuild The guild after the update - */ module.exports = GuildUpdateAction; diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index 9308aedb1..e34eabc2b 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -23,6 +23,11 @@ class MessageCreateAction extends Action { member.lastMessageChannelID = channel.id; } + /** + * Emitted whenever a message is created. + * @event Client#message + * @param {Message} message The created message + */ client.emit(Events.MESSAGE_CREATE, message); return { message }; } @@ -31,10 +36,5 @@ class MessageCreateAction extends Action { } } -/** - * Emitted whenever a message is created. - * @event Client#message - * @param {Message} message The created message - */ module.exports = MessageCreateAction; diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index c31c39cb4..cbe555e86 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -12,6 +12,11 @@ class MessageDeleteAction extends Action { if (message) { channel.messages.delete(message.id); message.deleted = true; + /** + * Emitted whenever a message is deleted. + * @event Client#messageDelete + * @param {Message} message The deleted message + */ client.emit(Events.MESSAGE_DELETE, message); } } @@ -20,10 +25,5 @@ class MessageDeleteAction extends Action { } } -/** - * Emitted whenever a message is deleted. - * @event Client#messageDelete - * @param {Message} message The deleted message - */ module.exports = MessageDeleteAction; diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index e12eaa73a..ac8d9b399 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -19,6 +19,11 @@ class MessageDeleteBulkAction extends Action { } } + /** + * Emitted whenever messages are deleted in bulk. + * @event Client#messageDeleteBulk + * @param {Collection} messages The deleted messages, mapped by their ID + */ if (messages.size > 0) client.emit(Events.MESSAGE_BULK_DELETE, messages); return { messages }; } @@ -26,10 +31,5 @@ class MessageDeleteBulkAction extends Action { } } -/** - * Emitted whenever messages are deleted in bulk. - * @event Client#messageDeleteBulk - * @param {Collection} messages The deleted messages, mapped by their ID - */ module.exports = MessageDeleteBulkAction; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 0708d3380..06b146df6 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -29,11 +29,4 @@ class MessageReactionAdd extends Action { } } -/** - * Emitted whenever a reaction is added to a cached message. - * @event Client#messageReactionAdd - * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user that applied the guild or reaction emoji - */ - module.exports = MessageReactionAdd; diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 63e349f7d..af76c4fd9 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -24,17 +24,17 @@ class MessageReactionRemove extends Action { const reaction = message.reactions.get(emojiID); if (!reaction) return false; reaction._remove(user); + /** + * Emitted whenever a reaction is removed from a cached message. + * @event Client#messageReactionRemove + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user whose emoji or reaction emoji was removed + */ this.client.emit(Events.MESSAGE_REACTION_REMOVE, reaction, user); return { message, reaction, user }; } } -/** - * Emitted whenever a reaction is removed from a cached message. - * @event Client#messageReactionRemove - * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user whose emoji or reaction emoji was removed - */ module.exports = MessageReactionRemove; diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 53b18c80a..538e0e8af 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -28,6 +28,12 @@ class PresenceUpdateAction extends Action { const old = member._clone(); if (member.presence) old.frozenPresence = member.presence._clone(); guild.presences.add(data); + /** + * Emitted whenever a guild member's presence changes, or they change one of their details. + * @event Client#presenceUpdate + * @param {GuildMember} oldMember The member before the presence update + * @param {GuildMember} newMember The member after the presence update + */ this.client.emit(Events.PRESENCE_UPDATE, old, member); } else { guild.presences.add(data); diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 0eb9c5a57..80e120cb2 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -27,16 +27,16 @@ class VoiceStateUpdate extends Action { client.emit('self.voiceStateUpdate', data); } + /** + * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. + * @event Client#voiceStateUpdate + * @param {?VoiceState} oldState The voice state before the update + * @param {VoiceState} newState The voice state after the update + */ client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); } } } -/** - * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. - * @event Client#voiceStateUpdate - * @param {?VoiceState} oldState The voice state before the update - * @param {VoiceState} newState The voice state after the update - */ module.exports = VoiceStateUpdate; diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 5ffc41a40..08ec838f1 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -5,14 +5,14 @@ class WebhooksUpdate extends Action { handle(data) { const client = this.client; const channel = client.channels.get(data.channel_id); + /** + * Emitted whenever a guild text channel has its webhooks changed. + * @event Client#webhookUpdate + * @param {TextChannel} channel The channel that had a webhook update + */ if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } } -/** - * Emitted whenever a guild text channel has its webhooks changed. - * @event Client#webhookUpdate - * @param {TextChannel} channel The channel that had a webhook update - */ module.exports = WebhooksUpdate; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js index d81762124..e2ac2c07a 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -2,5 +2,11 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { const { user, reaction } = client.actions.MessageReactionAdd.handle(packet.d); + /** + * Emitted whenever a reaction is added to a cached message. + * @event Client#messageReactionAdd + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user that applied the guild or reaction emoji + */ if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); }; From 23a16c3a7369d021e6b76db938831599fd14fa78 Mon Sep 17 00:00:00 2001 From: Darqam Date: Tue, 27 Nov 2018 21:41:34 +0100 Subject: [PATCH 0938/1359] fix(GuildChannel): add explicit channel resolve error to member edit (#2958) * Add explicit error to setting invalid voice channel * restrict to guild Co-Authored-By: Darqam * add a more explicit error and channel type check * bad tab --- src/errors/Messages.js | 1 + src/structures/GuildMember.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 2e2ec97bb..1abdd4ca0 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -84,6 +84,7 @@ const Messages = { MESSAGE_SPLIT_MISSING: 'Message exceeds the max length and contains no split characters.', GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', + GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', GUILD_OWNED: 'Guild is owned by the client.', GUILD_RESTRICTED: (state = false) => `Guild is ${state ? 'already' : 'not'} restricted.`, diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d910117d3..a2e82d793 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -248,7 +248,11 @@ class GuildMember extends Base { */ edit(data, reason) { if (data.channel) { - data.channel_id = this.client.channels.resolve(data.channel).id; + data.channel = this.guild.channels.resolve(data.channel); + if (!data.channel || data.channel.type !== 'voice') { + throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); + } + data.channel_id = data.channel.id; data.channel = null; } if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); From ecaec29380d31dcd94a30b4242bf26c5e4c395f7 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 27 Nov 2018 21:42:28 +0100 Subject: [PATCH 0939/1359] fix(Util): throw an explicit error if a chunk exceeds the max length (#2936) * fix(Util): throw an explicit error if a chunk exceeds the max length * refactor(Util): consolidate both errors in splitMessage into one * revert(Messages): do not unnecessarily change the error code * revert(Messages): do not remove the word 'the' --- src/errors/Messages.js | 4 +--- src/util/Util.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 1abdd4ca0..5f237ac5b 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -73,7 +73,7 @@ const Messages = { TYPING_COUNT: 'Count must be at least 1', - SPLIT_MAX_LEN: 'Message exceeds the max length and contains no split characters.', + SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user ID to ${ban ? 'ban' : 'unban'}.`, @@ -81,8 +81,6 @@ const Messages = { SEARCH_CHANNEL_TYPE: 'Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.', - MESSAGE_SPLIT_MISSING: 'Message exceeds the max length and contains no split characters.', - GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', diff --git a/src/util/Util.js b/src/util/Util.js index 119d0c7c7..562b81a41 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -56,7 +56,7 @@ class Util { static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { if (text.length <= maxLength) return text; const splitText = text.split(char); - if (splitText.length === 1) throw new RangeError('SPLIT_MAX_LEN'); + if (splitText.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); const messages = []; let msg = ''; for (const chunk of splitText) { From 42505b78c15306491ac3a605f86a3e010bc80958 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 3 Dec 2018 15:19:10 -0600 Subject: [PATCH 0940/1359] chore: add strict mode (#2974) --- .eslintrc.json | 1 + src/WebSocket.js | 2 ++ src/client/BaseClient.js | 2 ++ src/client/Client.js | 2 ++ src/client/WebhookClient.js | 2 ++ src/client/actions/Action.js | 2 ++ src/client/actions/ActionsManager.js | 2 ++ src/client/actions/ChannelCreate.js | 2 ++ src/client/actions/ChannelDelete.js | 2 ++ src/client/actions/ChannelUpdate.js | 2 ++ src/client/actions/GuildBanRemove.js | 2 ++ src/client/actions/GuildChannelsPositionUpdate.js | 2 ++ src/client/actions/GuildDelete.js | 2 ++ src/client/actions/GuildEmojiCreate.js | 2 ++ src/client/actions/GuildEmojiDelete.js | 2 ++ src/client/actions/GuildEmojiUpdate.js | 2 ++ src/client/actions/GuildEmojisUpdate.js | 2 ++ src/client/actions/GuildIntegrationsUpdate.js | 2 ++ src/client/actions/GuildMemberRemove.js | 2 ++ src/client/actions/GuildRoleCreate.js | 2 ++ src/client/actions/GuildRoleDelete.js | 2 ++ src/client/actions/GuildRoleUpdate.js | 2 ++ src/client/actions/GuildRolesPositionUpdate.js | 2 ++ src/client/actions/GuildUpdate.js | 2 ++ src/client/actions/MessageCreate.js | 2 ++ src/client/actions/MessageDelete.js | 2 ++ src/client/actions/MessageDeleteBulk.js | 2 ++ src/client/actions/MessageReactionAdd.js | 2 ++ src/client/actions/MessageReactionRemove.js | 2 ++ src/client/actions/MessageReactionRemoveAll.js | 2 ++ src/client/actions/MessageUpdate.js | 2 ++ src/client/actions/PresenceUpdate.js | 2 ++ src/client/actions/UserUpdate.js | 2 ++ src/client/actions/VoiceStateUpdate.js | 2 ++ src/client/actions/WebhooksUpdate.js | 2 ++ src/client/voice/ClientVoiceManager.js | 2 ++ src/client/voice/VoiceBroadcast.js | 2 ++ src/client/voice/VoiceConnection.js | 2 ++ src/client/voice/dispatcher/BroadcastDispatcher.js | 2 ++ src/client/voice/dispatcher/StreamDispatcher.js | 2 ++ src/client/voice/networking/VoiceUDPClient.js | 2 ++ src/client/voice/networking/VoiceWebSocket.js | 2 ++ src/client/voice/player/AudioPlayer.js | 2 ++ src/client/voice/player/BasePlayer.js | 2 ++ src/client/voice/player/BroadcastAudioPlayer.js | 2 ++ src/client/voice/receiver/PacketHandler.js | 2 ++ src/client/voice/receiver/Receiver.js | 2 ++ src/client/voice/util/DispatcherSet.js | 2 ++ src/client/voice/util/PlayInterface.js | 2 ++ src/client/voice/util/Secretbox.js | 2 ++ src/client/voice/util/Silence.js | 2 ++ src/client/voice/util/VolumeInterface.js | 2 ++ src/client/websocket/WebSocketManager.js | 2 ++ src/client/websocket/WebSocketShard.js | 2 ++ src/client/websocket/handlers/CHANNEL_CREATE.js | 2 ++ src/client/websocket/handlers/CHANNEL_DELETE.js | 2 ++ src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 ++ src/client/websocket/handlers/CHANNEL_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_BAN_ADD.js | 2 ++ src/client/websocket/handlers/GUILD_BAN_REMOVE.js | 2 ++ src/client/websocket/handlers/GUILD_CREATE.js | 2 ++ src/client/websocket/handlers/GUILD_DELETE.js | 2 ++ src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBER_ADD.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js | 2 ++ src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_ROLE_CREATE.js | 2 ++ src/client/websocket/handlers/GUILD_ROLE_DELETE.js | 2 ++ src/client/websocket/handlers/GUILD_ROLE_UPDATE.js | 2 ++ src/client/websocket/handlers/GUILD_SYNC.js | 2 ++ src/client/websocket/handlers/GUILD_UPDATE.js | 2 ++ src/client/websocket/handlers/MESSAGE_CREATE.js | 2 ++ src/client/websocket/handlers/MESSAGE_DELETE.js | 2 ++ src/client/websocket/handlers/MESSAGE_DELETE_BULK.js | 2 ++ src/client/websocket/handlers/MESSAGE_REACTION_ADD.js | 2 ++ src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js | 2 ++ src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js | 2 ++ src/client/websocket/handlers/MESSAGE_UPDATE.js | 2 ++ src/client/websocket/handlers/PRESENCE_UPDATE.js | 2 ++ src/client/websocket/handlers/READY.js | 2 ++ src/client/websocket/handlers/RESUMED.js | 2 ++ src/client/websocket/handlers/TYPING_START.js | 2 ++ src/client/websocket/handlers/USER_UPDATE.js | 2 ++ src/client/websocket/handlers/VOICE_SERVER_UPDATE.js | 2 ++ src/client/websocket/handlers/VOICE_STATE_UPDATE.js | 2 ++ src/client/websocket/handlers/WEBHOOKS_UPDATE.js | 2 ++ src/client/websocket/handlers/index.js | 2 ++ src/errors/DJSError.js | 2 ++ src/errors/Messages.js | 2 ++ src/errors/index.js | 2 ++ src/index.js | 2 ++ src/rest/APIRequest.js | 2 ++ src/rest/APIRouter.js | 2 ++ src/rest/DiscordAPIError.js | 2 ++ src/rest/HTTPError.js | 2 ++ src/rest/RESTManager.js | 2 ++ src/rest/RequestHandler.js | 2 ++ src/sharding/Shard.js | 2 ++ src/sharding/ShardClientUtil.js | 2 ++ src/sharding/ShardingManager.js | 2 ++ src/stores/ChannelStore.js | 2 ++ src/stores/DataStore.js | 2 ++ src/stores/GuildChannelStore.js | 2 ++ src/stores/GuildEmojiRoleStore.js | 2 ++ src/stores/GuildEmojiStore.js | 2 ++ src/stores/GuildMemberRoleStore.js | 2 ++ src/stores/GuildMemberStore.js | 2 ++ src/stores/GuildStore.js | 2 ++ src/stores/MessageStore.js | 2 ++ src/stores/PresenceStore.js | 2 ++ src/stores/ReactionStore.js | 2 ++ src/stores/ReactionUserStore.js | 2 ++ src/stores/RoleStore.js | 2 ++ src/stores/UserStore.js | 2 ++ src/stores/VoiceStateStore.js | 2 ++ src/structures/APIMessage.js | 2 ++ src/structures/Base.js | 2 ++ src/structures/CategoryChannel.js | 2 ++ src/structures/Channel.js | 2 ++ src/structures/ClientApplication.js | 2 ++ src/structures/ClientPresence.js | 2 ++ src/structures/ClientUser.js | 2 ++ src/structures/DMChannel.js | 2 ++ src/structures/Emoji.js | 2 ++ src/structures/GroupDMChannel.js | 2 ++ src/structures/Guild.js | 2 ++ src/structures/GuildAuditLogs.js | 2 ++ src/structures/GuildChannel.js | 2 ++ src/structures/GuildEmoji.js | 2 ++ src/structures/GuildMember.js | 2 ++ src/structures/Integration.js | 2 ++ src/structures/Invite.js | 2 ++ src/structures/Message.js | 2 ++ src/structures/MessageAttachment.js | 2 ++ src/structures/MessageCollector.js | 2 ++ src/structures/MessageEmbed.js | 2 ++ src/structures/MessageMentions.js | 2 ++ src/structures/MessageReaction.js | 2 ++ src/structures/PermissionOverwrites.js | 2 ++ src/structures/Presence.js | 2 ++ src/structures/ReactionCollector.js | 2 ++ src/structures/ReactionEmoji.js | 2 ++ src/structures/Role.js | 2 ++ src/structures/TextChannel.js | 2 ++ src/structures/User.js | 2 ++ src/structures/VoiceChannel.js | 2 ++ src/structures/VoiceRegion.js | 2 ++ src/structures/VoiceState.js | 2 ++ src/structures/Webhook.js | 2 ++ src/structures/interfaces/Collector.js | 2 ++ src/structures/interfaces/TextBasedChannel.js | 2 ++ src/util/ActivityFlags.js | 2 ++ src/util/BitField.js | 2 ++ src/util/Collection.js | 2 ++ src/util/Constants.js | 2 ++ src/util/DataResolver.js | 2 ++ src/util/Permissions.js | 2 ++ src/util/Snowflake.js | 2 ++ src/util/Speaking.js | 2 ++ src/util/Structures.js | 2 ++ src/util/Util.js | 2 ++ webpack.config.js | 2 ++ 164 files changed, 327 insertions(+) diff --git a/.eslintrc.json b/.eslintrc.json index 9b7c229b6..a4f08d689 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,6 +11,7 @@ { "files": ["*.browser.js"], "env": { "browser": true } } ], "rules": { + "strict": ["error", "global"], "no-await-in-loop": "warn", "no-compare-neg-zero": "error", "no-extra-parens": ["warn", "all", { diff --git a/src/WebSocket.js b/src/WebSocket.js index c6f4209fb..2a303cc46 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -1,3 +1,5 @@ +'use strict'; + const { browser } = require('./util/Constants'); const querystring = require('querystring'); try { diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index c9fce5908..68621360e 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -1,3 +1,5 @@ +'use strict'; + require('setimmediate'); const EventEmitter = require('events'); const RESTManager = require('../rest/RESTManager'); diff --git a/src/client/Client.js b/src/client/Client.js index 5d5deddf6..1bc8232b8 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -1,3 +1,5 @@ +'use strict'; + const BaseClient = require('./BaseClient'); const Permissions = require('../util/Permissions'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); diff --git a/src/client/WebhookClient.js b/src/client/WebhookClient.js index 0a848d808..94e17a71d 100644 --- a/src/client/WebhookClient.js +++ b/src/client/WebhookClient.js @@ -1,3 +1,5 @@ +'use strict'; + const Webhook = require('../structures/Webhook'); const BaseClient = require('./BaseClient'); diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 8fdadc92b..791eaa00c 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -1,3 +1,5 @@ +'use strict'; + /* ABOUT ACTIONS diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index f349b9bcd..268e4371b 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -1,3 +1,5 @@ +'use strict'; + class ActionsManager { constructor(client) { this.client = client; diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 57b4e6dfd..4a9d17d45 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index 7f182e60c..b9909f8ba 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index ac2d4f69a..b610ea7ca 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class ChannelUpdateAction extends Action { diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 7d19c75ba..62779137f 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildChannelsPositionUpdate.js b/src/client/actions/GuildChannelsPositionUpdate.js index 863d7dc51..b21115947 100644 --- a/src/client/actions/GuildChannelsPositionUpdate.js +++ b/src/client/actions/GuildChannelsPositionUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class GuildChannelsPositionUpdate extends Action { diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 27ea1b001..7c61ebacd 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojiCreate.js b/src/client/actions/GuildEmojiCreate.js index 121e89ab9..379c62e0e 100644 --- a/src/client/actions/GuildEmojiCreate.js +++ b/src/client/actions/GuildEmojiCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index 7b94ff41c..d5c973afa 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojiUpdate.js b/src/client/actions/GuildEmojiUpdate.js index 34e235a0a..9fa59c96a 100644 --- a/src/client/actions/GuildEmojiUpdate.js +++ b/src/client/actions/GuildEmojiUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildEmojisUpdate.js b/src/client/actions/GuildEmojisUpdate.js index aaa898477..d6902d535 100644 --- a/src/client/actions/GuildEmojisUpdate.js +++ b/src/client/actions/GuildEmojisUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class GuildEmojisUpdateAction extends Action { diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js index 8bb6eabf3..a8e910774 100644 --- a/src/client/actions/GuildIntegrationsUpdate.js +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 62373e486..6a4ae397e 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events, Status } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index e4657213b..36111f064 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js index 0cecc8efe..31b13b812 100644 --- a/src/client/actions/GuildRoleDelete.js +++ b/src/client/actions/GuildRoleDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js index c2e082c70..bf61c7878 100644 --- a/src/client/actions/GuildRoleUpdate.js +++ b/src/client/actions/GuildRoleUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/GuildRolesPositionUpdate.js b/src/client/actions/GuildRolesPositionUpdate.js index af29702ad..f09f11436 100644 --- a/src/client/actions/GuildRolesPositionUpdate.js +++ b/src/client/actions/GuildRolesPositionUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class GuildRolesPositionUpdate extends Action { diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js index ac592d277..6d7cf9b4e 100644 --- a/src/client/actions/GuildUpdate.js +++ b/src/client/actions/GuildUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index e34eabc2b..d4ef64001 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index cbe555e86..d66d10a28 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index ac8d9b399..53f4ba051 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const Collection = require('../../util/Collection'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 06b146df6..a800c3c7a 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); /* diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index af76c4fd9..7ab5be289 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index ac03c6e83..f32730977 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js index 4fbdce4e4..be26b2692 100644 --- a/src/client/actions/MessageUpdate.js +++ b/src/client/actions/MessageUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); class MessageUpdateAction extends Action { diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 538e0e8af..649bf6ce0 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index 3ff407901..a762511f4 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 80e120cb2..e4efa1292 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); const VoiceState = require('../../structures/VoiceState'); diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 08ec838f1..69e28aecb 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -1,3 +1,5 @@ +'use strict'; + const Action = require('./Action'); const { Events } = require('../../util/Constants'); diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index d55a809f2..a9081efd5 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../../util/Collection'); const { VoiceStatus } = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 1bc4e2827..602f59205 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer'); const DispatcherSet = require('./util/DispatcherSet'); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6a4d4cdaa..078781b53 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -1,3 +1,5 @@ +'use strict'; + const VoiceWebSocket = require('./networking/VoiceWebSocket'); const VoiceUDP = require('./networking/VoiceUDPClient'); const Util = require('../../util/Util'); diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index 90cff6a40..e5c7650cf 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -1,3 +1,5 @@ +'use strict'; + const StreamDispatcher = require('./StreamDispatcher'); /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 7bb0ef1b0..93c6df81f 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -1,3 +1,5 @@ +'use strict'; + const VolumeInterface = require('../util/VolumeInterface'); const { Writable } = require('stream'); diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index fe34cab33..c1dd9e3d0 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -1,3 +1,5 @@ +'use strict'; + const udp = require('dgram'); const { VoiceOPCodes } = require('../../../util/Constants'); const EventEmitter = require('events'); diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index bd03ea54a..d556145b4 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -1,3 +1,5 @@ +'use strict'; + const { OPCodes, VoiceOPCodes } = require('../../../util/Constants'); const EventEmitter = require('events'); const { Error } = require('../../../errors'); diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index e3381f5cc..3ce94d815 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -1,3 +1,5 @@ +'use strict'; + const BasePlayer = require('./BasePlayer'); /** diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index e19984dff..c9c6cf0d1 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events').EventEmitter; const { Readable: ReadableStream } = require('stream'); const prism = require('prism-media'); diff --git a/src/client/voice/player/BroadcastAudioPlayer.js b/src/client/voice/player/BroadcastAudioPlayer.js index 052c9ea0b..c1cab9f41 100644 --- a/src/client/voice/player/BroadcastAudioPlayer.js +++ b/src/client/voice/player/BroadcastAudioPlayer.js @@ -1,3 +1,5 @@ +'use strict'; + const BroadcastDispatcher = require('../dispatcher/BroadcastDispatcher'); const BasePlayer = require('./BasePlayer'); diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 1ea702c1f..7189f1126 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -1,3 +1,5 @@ +'use strict'; + const secretbox = require('../util/Secretbox'); const EventEmitter = require('events'); diff --git a/src/client/voice/receiver/Receiver.js b/src/client/voice/receiver/Receiver.js index 1174f70bf..605d99271 100644 --- a/src/client/voice/receiver/Receiver.js +++ b/src/client/voice/receiver/Receiver.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const prism = require('prism-media'); const PacketHandler = require('./PacketHandler'); diff --git a/src/client/voice/util/DispatcherSet.js b/src/client/voice/util/DispatcherSet.js index a1ab7e943..38a7c8b6a 100644 --- a/src/client/voice/util/DispatcherSet.js +++ b/src/client/voice/util/DispatcherSet.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); /** diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 58d31141e..ac66eb290 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -1,3 +1,5 @@ +'use strict'; + const { Readable } = require('stream'); const prism = require('prism-media'); const { Error } = require('../../../errors'); diff --git a/src/client/voice/util/Secretbox.js b/src/client/voice/util/Secretbox.js index fe1eebe84..1b30eeb60 100644 --- a/src/client/voice/util/Secretbox.js +++ b/src/client/voice/util/Secretbox.js @@ -1,3 +1,5 @@ +'use strict'; + const libs = { sodium: sodium => ({ open: sodium.api.crypto_secretbox_open_easy, diff --git a/src/client/voice/util/Silence.js b/src/client/voice/util/Silence.js index b9643da17..2068d7630 100644 --- a/src/client/voice/util/Silence.js +++ b/src/client/voice/util/Silence.js @@ -1,3 +1,5 @@ +'use strict'; + const { Readable } = require('stream'); const SILENCE_FRAME = Buffer.from([0xF8, 0xFF, 0xFE]); diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index 95645f91e..a631e6caf 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); /** diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 891da3f45..fd7499682 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../../util/Collection'); const WebSocketShard = require('./WebSocketShard'); const { Events, Status, WSEvents } = require('../../util/Constants'); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 7aec37e25..6237b0516 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); diff --git a/src/client/websocket/handlers/CHANNEL_CREATE.js b/src/client/websocket/handlers/CHANNEL_CREATE.js index 3074254fc..d6d560d8e 100644 --- a/src/client/websocket/handlers/CHANNEL_CREATE.js +++ b/src/client/websocket/handlers/CHANNEL_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.ChannelCreate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/CHANNEL_DELETE.js b/src/client/websocket/handlers/CHANNEL_DELETE.js index 158ccb352..cb9f3d8c1 100644 --- a/src/client/websocket/handlers/CHANNEL_DELETE.js +++ b/src/client/websocket/handlers/CHANNEL_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.ChannelDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index 1272dbbb1..dfc854e37 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js index 46d9037a2..b437ebbde 100644 --- a/src/client/websocket/handlers/CHANNEL_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js index 00772c8cc..4fa89edcf 100644 --- a/src/client/websocket/handlers/GUILD_BAN_ADD.js +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { diff --git a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js index 08483a835..8389e46e8 100644 --- a/src/client/websocket/handlers/GUILD_BAN_REMOVE.js +++ b/src/client/websocket/handlers/GUILD_BAN_REMOVE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildBanRemove.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 05250ed6e..9f13c6581 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events, Status } = require('../../../util/Constants'); module.exports = async (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/GUILD_DELETE.js b/src/client/websocket/handlers/GUILD_DELETE.js index 19d1b3b0a..27a325623 100644 --- a/src/client/websocket/handlers/GUILD_DELETE.js +++ b/src/client/websocket/handlers/GUILD_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js index 5fa5a9d54..e23b6713f 100644 --- a/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_EMOJIS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildEmojisUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js index 6c1a0cfd3..e90a72c25 100644 --- a/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_INTEGRATIONS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildIntegrationsUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js index 7178264d2..0738eaa67 100644 --- a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); const Collection = require('../../../util/Collection'); diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js index 367058a5b..796b6c376 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events, Status } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js index b00da0e63..72432af11 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet, shard) => { client.actions.GuildMemberRemove.handle(packet.d, shard); }; diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js index be4f573ad..9341329a3 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Status, Events } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js index b6ea80381..da9e7bc4b 100644 --- a/src/client/websocket/handlers/GUILD_ROLE_CREATE.js +++ b/src/client/websocket/handlers/GUILD_ROLE_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildRoleCreate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js index d1093cb27..cdc635316 100644 --- a/src/client/websocket/handlers/GUILD_ROLE_DELETE.js +++ b/src/client/websocket/handlers/GUILD_ROLE_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildRoleDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js index c1f526c57..3a9b62e80 100644 --- a/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_ROLE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildRoleUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_SYNC.js b/src/client/websocket/handlers/GUILD_SYNC.js index f27da424e..b7e7d1b24 100644 --- a/src/client/websocket/handlers/GUILD_SYNC.js +++ b/src/client/websocket/handlers/GUILD_SYNC.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildSync.handle(packet.d); }; diff --git a/src/client/websocket/handlers/GUILD_UPDATE.js b/src/client/websocket/handlers/GUILD_UPDATE.js index 0f3e24f74..fd0012ad9 100644 --- a/src/client/websocket/handlers/GUILD_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.GuildUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_CREATE.js b/src/client/websocket/handlers/MESSAGE_CREATE.js index bc9303fd5..c9b79a8fa 100644 --- a/src/client/websocket/handlers/MESSAGE_CREATE.js +++ b/src/client/websocket/handlers/MESSAGE_CREATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageCreate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE.js b/src/client/websocket/handlers/MESSAGE_DELETE.js index 09062196c..85ae2bc75 100644 --- a/src/client/websocket/handlers/MESSAGE_DELETE.js +++ b/src/client/websocket/handlers/MESSAGE_DELETE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageDelete.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js index a927b3b14..fbcf80fd3 100644 --- a/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js +++ b/src/client/websocket/handlers/MESSAGE_DELETE_BULK.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageDeleteBulk.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js index e2ac2c07a..6d0bbb059 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js index 8b9f22a17..2980e6954 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageReactionRemove.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js index 2323cfe05..ead80f755 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_ALL.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.MessageReactionRemoveAll.handle(packet.d); }; diff --git a/src/client/websocket/handlers/MESSAGE_UPDATE.js b/src/client/websocket/handlers/MESSAGE_UPDATE.js index 9be750c7e..7428e90c8 100644 --- a/src/client/websocket/handlers/MESSAGE_UPDATE.js +++ b/src/client/websocket/handlers/MESSAGE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet) => { diff --git a/src/client/websocket/handlers/PRESENCE_UPDATE.js b/src/client/websocket/handlers/PRESENCE_UPDATE.js index 89b9f0e25..bde36297d 100644 --- a/src/client/websocket/handlers/PRESENCE_UPDATE.js +++ b/src/client/websocket/handlers/PRESENCE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.PresenceUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index f22968d3d..74b1e1b9a 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -1,3 +1,5 @@ +'use strict'; + let ClientUser; module.exports = (client, { d: data }, shard) => { diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js index 6cc355e98..bd00ec883 100644 --- a/src/client/websocket/handlers/RESUMED.js +++ b/src/client/websocket/handlers/RESUMED.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, packet, shard) => { diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js index ac01d30d9..9df76dc75 100644 --- a/src/client/websocket/handlers/TYPING_START.js +++ b/src/client/websocket/handlers/TYPING_START.js @@ -1,3 +1,5 @@ +'use strict'; + const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { diff --git a/src/client/websocket/handlers/USER_UPDATE.js b/src/client/websocket/handlers/USER_UPDATE.js index 3c5b859c3..a02bf5887 100644 --- a/src/client/websocket/handlers/USER_UPDATE.js +++ b/src/client/websocket/handlers/USER_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.UserUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js index c8ac3883c..2663d99aa 100644 --- a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.emit('self.voiceServer', packet.d); }; diff --git a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js index a9527adaa..dbff6ea2d 100644 --- a/src/client/websocket/handlers/VOICE_STATE_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_STATE_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.VoiceStateUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js index b1afb91c5..46cacee0b 100644 --- a/src/client/websocket/handlers/WEBHOOKS_UPDATE.js +++ b/src/client/websocket/handlers/WEBHOOKS_UPDATE.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = (client, packet) => { client.actions.WebhooksUpdate.handle(packet.d); }; diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index 77bcf8fd0..b253cefee 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -1,3 +1,5 @@ +'use strict'; + const { WSEvents } = require('../../../util/Constants'); const handlers = {}; diff --git a/src/errors/DJSError.js b/src/errors/DJSError.js index 1d5aea124..157ca6600 100644 --- a/src/errors/DJSError.js +++ b/src/errors/DJSError.js @@ -1,3 +1,5 @@ +'use strict'; + // Heavily inspired by node's `internal/errors` module const kCode = Symbol('code'); diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 5f237ac5b..792c2b8b9 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -1,3 +1,5 @@ +'use strict'; + const { register } = require('./DJSError'); const Messages = { diff --git a/src/errors/index.js b/src/errors/index.js index 39b7582df..c94ddc788 100644 --- a/src/errors/index.js +++ b/src/errors/index.js @@ -1,2 +1,4 @@ +'use strict'; + module.exports = require('./DJSError'); module.exports.Messages = require('./Messages'); diff --git a/src/index.js b/src/index.js index a124108f6..1747f68c6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('./util/Util'); module.exports = { diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index ec8375dd1..7b0a73e41 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -1,3 +1,5 @@ +'use strict'; + const querystring = require('querystring'); const FormData = require('form-data'); const https = require('https'); diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 06ebeb3f9..715b1c7c4 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -1,3 +1,5 @@ +'use strict'; + const noop = () => {}; // eslint-disable-line no-empty-function const methods = ['get', 'post', 'delete', 'patch', 'put']; const reflectors = [ diff --git a/src/rest/DiscordAPIError.js b/src/rest/DiscordAPIError.js index c90ee4f5a..559194f2c 100644 --- a/src/rest/DiscordAPIError.js +++ b/src/rest/DiscordAPIError.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Represents an error from the Discord API. * @extends Error diff --git a/src/rest/HTTPError.js b/src/rest/HTTPError.js index 32b3e6bd8..2911467fc 100644 --- a/src/rest/HTTPError.js +++ b/src/rest/HTTPError.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Represents a HTTP error from a request. * @extends Error diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 53a8047f5..306fa2468 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -1,3 +1,5 @@ +'use strict'; + const RequestHandler = require('./RequestHandler'); const APIRequest = require('./APIRequest'); const routeBuilder = require('./APIRouter'); diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 3f66fd438..44fca4fbc 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -1,3 +1,5 @@ +'use strict'; + const DiscordAPIError = require('./DiscordAPIError'); const HTTPError = require('./HTTPError'); const Util = require('../util/Util'); diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 3b25c1d0a..adc5caeb9 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -1,3 +1,5 @@ +'use strict'; + const EventEmitter = require('events'); const path = require('path'); const Util = require('../util/Util'); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 201568ca1..026c97efa 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const { Events } = require('../util/Constants'); diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index b71962290..aca3ebed3 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); const fs = require('fs'); const EventEmitter = require('events'); diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 6e2e4081a..8d491c837 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const Channel = require('../structures/Channel'); const { Events } = require('../util/Constants'); diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index 3708893c9..fc841899a 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); let Structures; diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index e67bcc1c3..e538889da 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('../structures/Channel'); const { ChannelTypes } = require('../util/Constants'); const DataStore = require('./DataStore'); diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index e2fe5ecdd..8c097f564 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Util = require('../util/Util'); const { TypeError } = require('../errors'); diff --git a/src/stores/GuildEmojiStore.js b/src/stores/GuildEmojiStore.js index 925f79ab9..b992a68c9 100644 --- a/src/stores/GuildEmojiStore.js +++ b/src/stores/GuildEmojiStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const DataStore = require('./DataStore'); const GuildEmoji = require('../structures/GuildEmoji'); diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 205de123f..8789df212 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Util = require('../util/Util'); const { TypeError } = require('../errors'); diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 6f95aec92..039fb4c7b 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const GuildMember = require('../structures/GuildMember'); const { Events, OPCodes } = require('../util/Constants'); diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 0cf9a1157..90e4358dd 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const DataResolver = require('../util/DataResolver'); const { Events } = require('../util/Constants'); diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 2d7049792..1a8a7083a 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const Collection = require('../util/Collection'); const Message = require('../structures/Message'); diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index 15fc9f124..061d3f1e9 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const { Presence } = require('../structures/Presence'); diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 38c467b79..b3910ba4c 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const MessageReaction = require('../structures/MessageReaction'); diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js index 1b0275795..976b79917 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/stores/ReactionUserStore.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const DataStore = require('./DataStore'); const { Error } = require('../errors'); diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 3c3a5b35a..14956c309 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const Role = require('../structures/Role'); const { resolveColor } = require('../util/Util'); diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index d700877c5..8fd2d9d5a 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const User = require('../structures/User'); const GuildMember = require('../structures/GuildMember'); diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js index 2de492495..ece3c2bb6 100644 --- a/src/stores/VoiceStateStore.js +++ b/src/stores/VoiceStateStore.js @@ -1,3 +1,5 @@ +'use strict'; + const DataStore = require('./DataStore'); const VoiceState = require('../structures/VoiceState'); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 8b714fcd5..deca5d26d 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -1,3 +1,5 @@ +'use strict'; + const DataResolver = require('../util/DataResolver'); const MessageEmbed = require('./MessageEmbed'); const MessageAttachment = require('./MessageAttachment'); diff --git a/src/structures/Base.js b/src/structures/Base.js index 37633757b..4850a39d5 100644 --- a/src/structures/Base.js +++ b/src/structures/Base.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); /** diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index 5766a4a75..60be408c6 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildChannel = require('./GuildChannel'); /** diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 8466d19af..92914118d 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const Base = require('./Base'); const { ChannelTypes } = require('../util/Constants'); diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 7ea360200..d2bcd17a7 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants'); const Base = require('./Base'); diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 68dda64c5..a92dfb302 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -1,3 +1,5 @@ +'use strict'; + const { Presence } = require('./Presence'); const Collection = require('../util/Collection'); const { ActivityTypes, OPCodes } = require('../util/Constants'); diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 1b8d90d98..79d7ad0e1 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,3 +1,5 @@ +'use strict'; + const Structures = require('../util/Structures'); const DataResolver = require('../util/DataResolver'); diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 1f45f490f..5549618a2 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const MessageStore = require('../stores/MessageStore'); diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index ed844f18a..803fc5002 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const Base = require('./Base'); diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index b2fe7200f..6f9aca390 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a194ea15a..aef0b3113 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -1,3 +1,5 @@ +'use strict'; + const Invite = require('./Invite'); const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index a5212d5b4..f78078a7c 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index daf20a52d..dc0a4e415 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const Channel = require('./Channel'); const Role = require('./Role'); const Invite = require('./Invite'); diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 93e8d275d..24fd7c02a 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore'); const Permissions = require('../util/Permissions'); const { Error } = require('../errors'); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index a2e82d793..d91071c93 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -1,3 +1,5 @@ +'use strict'; + const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Role = require('./Role'); const Permissions = require('../util/Permissions'); diff --git a/src/structures/Integration.js b/src/structures/Integration.js index 2782008ba..5ff760dc7 100644 --- a/src/structures/Integration.js +++ b/src/structures/Integration.js @@ -1,3 +1,5 @@ +'use strict'; + const Base = require('./Base'); /** diff --git a/src/structures/Invite.js b/src/structures/Invite.js index b142d6960..fec3f2d7d 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -1,3 +1,5 @@ +'use strict'; + const { Endpoints } = require('../util/Constants'); const Base = require('./Base'); diff --git a/src/structures/Message.js b/src/structures/Message.js index 88a1707a9..1947705ec 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -1,3 +1,5 @@ +'use strict'; + const Mentions = require('./MessageMentions'); const MessageAttachment = require('./MessageAttachment'); const Embed = require('./MessageEmbed'); diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index cce6e487b..a4c053036 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); /** diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 3d9fcf727..59120b9db 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -1,3 +1,5 @@ +'use strict'; + const Collector = require('./interfaces/Collector'); const { Events } = require('../util/Constants'); diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 49ddf30a7..213a27a1d 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const { RangeError } = require('../errors'); diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index bd99397d7..b4414a3bb 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../util/Collection'); const Util = require('../util/Util'); const GuildMember = require('./GuildMember'); diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index faf3bfbfb..69138fda2 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildEmoji = require('./GuildEmoji'); const Util = require('../util/Util'); const ReactionEmoji = require('./ReactionEmoji'); diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index b28517864..cf11eaa94 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -1,3 +1,5 @@ +'use strict'; + const Role = require('./Role'); const Permissions = require('../util/Permissions'); const Util = require('../util/Util'); diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 339981340..122b461d5 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const ActivityFlags = require('../util/ActivityFlags'); const { ActivityTypes } = require('../util/Constants'); diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index 5c4532e2f..b4826100f 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -1,3 +1,5 @@ +'use strict'; + const Collector = require('./interfaces/Collector'); const Collection = require('../util/Collection'); const { Events } = require('../util/Constants'); diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index c6ea8d68f..2661fa91d 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); const Emoji = require('./Emoji'); diff --git a/src/structures/Role.js b/src/structures/Role.js index 18f68f2f9..cbae56969 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -1,3 +1,5 @@ +'use strict'; + const Snowflake = require('../util/Snowflake'); const Permissions = require('../util/Permissions'); const Util = require('../util/Util'); diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 6e053e5d0..47c8e19c2 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildChannel = require('./GuildChannel'); const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); diff --git a/src/structures/User.js b/src/structures/User.js index 673663234..22ea5c81a 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -1,3 +1,5 @@ +'use strict'; + const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { Presence } = require('./Presence'); const Snowflake = require('../util/Snowflake'); diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index aee01b6f0..cf9b1ee99 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const GuildChannel = require('./GuildChannel'); const { browser } = require('../util/Constants'); const Permissions = require('../util/Permissions'); diff --git a/src/structures/VoiceRegion.js b/src/structures/VoiceRegion.js index b4db21770..9626195da 100644 --- a/src/structures/VoiceRegion.js +++ b/src/structures/VoiceRegion.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); /** diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 88e0ed196..82f9eec71 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -1,3 +1,5 @@ +'use strict'; + const Base = require('./Base'); /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index c12cc67f8..7b851c770 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,3 +1,5 @@ +'use strict'; + const DataResolver = require('../util/DataResolver'); const Channel = require('./Channel'); const APIMessage = require('./APIMessage'); diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index c490ebb3c..1dbd61429 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -1,3 +1,5 @@ +'use strict'; + const Collection = require('../../util/Collection'); const Util = require('../../util/Util'); const EventEmitter = require('events'); diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index f9c668de3..075bc6752 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,3 +1,5 @@ +'use strict'; + const MessageCollector = require('../MessageCollector'); const Snowflake = require('../../util/Snowflake'); const Collection = require('../../util/Collection'); diff --git a/src/util/ActivityFlags.js b/src/util/ActivityFlags.js index 49fd7a678..f6f5aac9e 100644 --- a/src/util/ActivityFlags.js +++ b/src/util/ActivityFlags.js @@ -1,3 +1,5 @@ +'use strict'; + const BitField = require('./BitField'); /** diff --git a/src/util/BitField.js b/src/util/BitField.js index fa506a2e8..96b07b829 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -1,3 +1,5 @@ +'use strict'; + const { RangeError } = require('../errors'); /** diff --git a/src/util/Collection.js b/src/util/Collection.js index 7f0570d6c..3d01ffef0 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('./Util'); /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 01ff57d6f..aac3f5b34 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1,3 +1,5 @@ +'use strict'; + const Package = exports.Package = require('../../package.json'); const { Error, RangeError } = require('../errors'); const browser = exports.browser = typeof window !== 'undefined'; diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 5fcfd0d43..1b9057270 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); const fs = require('fs'); const fetch = require('node-fetch'); diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 295346b16..9f01df399 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -1,3 +1,5 @@ +'use strict'; + const BitField = require('./BitField'); /** diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js index 00f22d06f..06612c30b 100644 --- a/src/util/Snowflake.js +++ b/src/util/Snowflake.js @@ -1,3 +1,5 @@ +'use strict'; + const Util = require('../util/Util'); // Discord epoch (2015-01-01T00:00:00.000Z) diff --git a/src/util/Speaking.js b/src/util/Speaking.js index e0e557eaa..c706d9953 100644 --- a/src/util/Speaking.js +++ b/src/util/Speaking.js @@ -1,3 +1,5 @@ +'use strict'; + const BitField = require('./BitField'); /** diff --git a/src/util/Structures.js b/src/util/Structures.js index 8b1edd8b5..5bdf5c86a 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -1,3 +1,5 @@ +'use strict'; + /** * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}. */ diff --git a/src/util/Util.js b/src/util/Util.js index 562b81a41..1d7b7aeeb 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -1,3 +1,5 @@ +'use strict'; + const { Colors, DefaultOptions, Endpoints } = require('./Constants'); const fetch = require('node-fetch'); const { Error: DiscordError, RangeError, TypeError } = require('../errors'); diff --git a/webpack.config.js b/webpack.config.js index c7416852d..c95745f11 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +'use strict'; + const path = require('path'); const webpack = require('webpack'); const TerserJSPlugin = require('terser-webpack-plugin'); From b5d5c699e65bf14a77065a75aabd24e4972988a9 Mon Sep 17 00:00:00 2001 From: August Date: Sun, 9 Dec 2018 01:30:46 -0700 Subject: [PATCH 0941/1359] fix: guildBanRemove event name (#2983) - "Events.GUILD_BAN_REMOVEGUILD_BAN_REMOVE" -> "Events.GUILD_BAN_REMOVE" --- src/client/actions/GuildBanRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 62779137f..5a4c0a902 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -14,7 +14,7 @@ class GuildBanRemove extends Action { * @param {Guild} guild The guild that the unban occurred in * @param {User} user The user that was unbanned */ - if (guild && user) client.emit(Events.GUILD_BAN_REMOVEGUILD_BAN_REMOVE, guild, user); + if (guild && user) client.emit(Events.GUILD_BAN_REMOVE, guild, user); } } From 5cbdf380289f0fd1d16330325dab71f8815396fc Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Fri, 21 Dec 2018 23:49:56 -0800 Subject: [PATCH 0942/1359] fix(WebSocketShard): add websocket send error handling (#2981) * websocket send error handling * fix: emit only when error is present * refactor: use an if instead --- src/client/websocket/WebSocketShard.js | 4 +++- test/tester1000.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 6237b0516..ea4595b41 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -437,7 +437,9 @@ class WebSocketShard extends EventEmitter { this.debug(`Tried to send packet ${data} but no WebSocket is available!`); return; } - this.ws.send(WebSocket.pack(data)); + this.ws.send(WebSocket.pack(data), err => { + if (err) this.manager.client.emit(Events.ERROR, err); + }); } /** diff --git a/test/tester1000.js b/test/tester1000.js index d726188c5..f9d7f668d 100644 --- a/test/tester1000.js +++ b/test/tester1000.js @@ -13,6 +13,7 @@ client.on('ready', () => { log('READY', client.user.tag, client.user.id); }); client.on('rateLimit', log); +client.on('error', console.error); const commands = { eval: message => { From 8286d1a0fc3e86a10b7d5f977836b0e0fe611c27 Mon Sep 17 00:00:00 2001 From: Lucas Kellar Date: Sun, 23 Dec 2018 16:16:28 -0600 Subject: [PATCH 0943/1359] typings(SnowflakeUtil): add optional "timestamp" parameter to generate (#2998) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index ea8d55e7f..59e7bf515 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -991,7 +991,7 @@ declare module 'discord.js' { export class SnowflakeUtil { public static deconstruct(snowflake: Snowflake): DeconstructedSnowflake; - public static generate(): Snowflake; + public static generate(timestamp?: number | Date): Snowflake; } const VolumeMixin: (base: Constructable) => Constructable; From 8a76cc5c725ffcfe8131c652de3d16be1f51439a Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 25 Dec 2018 22:00:46 +0100 Subject: [PATCH 0944/1359] typings(TextBasedChannel): add `Snowflake[]` overload to bulkDelete (#3001) * typings: Add `string[]` overload to bulkDelete * misc: Requested changes --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 59e7bf515..a97225b7f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1441,7 +1441,7 @@ declare module 'discord.js' { typing: boolean; typingCount: number; awaitMessages(filter: CollectorFilter, options?: AwaitMessagesOptions): Promise>; - bulkDelete(messages: Collection | Message[] | number, filterOld?: boolean): Promise>; + bulkDelete(messages: Collection | Message[] | Snowflake[] | number, filterOld?: boolean): Promise>; createMessageCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; From 0bde9ca2c58785f2e1093698268aa9bdc525f5a5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 27 Dec 2018 18:05:54 +0000 Subject: [PATCH 0945/1359] voice: set seek parameter before input (#2993) --- src/client/voice/player/BasePlayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index c9c6cf0d1..763762ef6 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -48,7 +48,7 @@ class BasePlayer extends EventEmitter { const isStream = input instanceof ReadableStream; const args = isStream ? FFMPEG_ARGUMENTS.slice() : ['-i', input, ...FFMPEG_ARGUMENTS]; - if (options.seek) args.push('-ss', String(options.seek)); + if (options.seek) args.unshift('-ss', String(options.seek)); const ffmpeg = new prism.FFmpeg({ args }); const streams = { ffmpeg }; From 840d22f8920f7648f97109d3df6f8a9b1da9f20f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 30 Dec 2018 12:52:33 +0100 Subject: [PATCH 0946/1359] docs(Webhook): add mising '@name' to Webhook#token's docstring --- src/structures/Webhook.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 7b851c770..472de07ea 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -28,6 +28,7 @@ class Webhook { /** * The token for the webhook + * @name Webhook#token * @type {string} */ Object.defineProperty(this, 'token', { value: data.token, writable: true, configurable: true }); From 28db5273708af039ef6a5387273d4407d55f178c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 10 Jan 2019 16:54:02 +0100 Subject: [PATCH 0947/1359] docs(Guild): use 'updated' variable in example for setRegion --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index aef0b3113..66cf98718 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -725,7 +725,7 @@ class Guild extends Base { * @example * // Edit the guild region * guild.setRegion('london') - * .then(updated => console.log(`Updated guild region to ${guild.region}`)) + * .then(updated => console.log(`Updated guild region to ${updated.region}`)) * .catch(console.error); */ setRegion(region, reason) { From 8230255c68b94d68a4e8ffc559a98d08d1a08a7c Mon Sep 17 00:00:00 2001 From: Isabella Date: Tue, 15 Jan 2019 01:45:29 -0600 Subject: [PATCH 0948/1359] fix(ShardClientUtil#id): erroneously reporting as an array --- src/client/Client.js | 4 ++-- src/util/Constants.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 1bc8232b8..4e5f5e247 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -55,7 +55,7 @@ class Client extends BaseClient { this.options.totalShardCount = this.options.shardCount; } } - if (!this.options.shards && this.options.shardCount) { + if (typeof this.options.shards === 'undefined' && this.options.shardCount) { this.options.shards = []; for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); } @@ -233,7 +233,7 @@ class Client extends BaseClient { this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); this.options.shardCount = res.shards; this.options.totalShardCount = res.shards; - if (!this.options.shards || !this.options.shards.length) { + if (typeof this.options.shards === 'undefined' || !this.options.shards.length) { this.options.shards = []; for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); } diff --git a/src/util/Constants.js b/src/util/Constants.js index aac3f5b34..508b5f97d 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -7,7 +7,7 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number|number[]} [shards=0] ID of the shard to run, or an array of shard IDs + * @property {number|number[]} [shards] ID of the shard to run, or an array of shard IDs * @property {number} [shardCount=1] Total number of shards that will be spawned by this Client * @property {number} [totalShardCount=1] The total amount of shards used by all processes of this bot * (e.g. recommended shard count, shard count of the ShardingManager) @@ -37,7 +37,6 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {HTTPOptions} [http] HTTP options */ exports.DefaultOptions = { - shards: 0, shardCount: 1, totalShardCount: 1, messageCacheMaxSize: 200, From 3dff5058f0beae01cec0abe27e3a43c8fd7c60ce Mon Sep 17 00:00:00 2001 From: Anthony Collier Date: Mon, 21 Jan 2019 11:22:24 -0500 Subject: [PATCH 0949/1359] docs(Examples): fix usage of removed overload of Collection#find (#3027) --- docs/examples/greeting.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/greeting.js b/docs/examples/greeting.js index 142889227..8fc1dfada 100644 --- a/docs/examples/greeting.js +++ b/docs/examples/greeting.js @@ -19,7 +19,7 @@ client.on('ready', () => { // Create an event listener for new guild members client.on('guildMemberAdd', member => { // Send the message to a designated channel on a server: - const channel = member.guild.channels.find('name', 'member-log'); + const channel = member.guild.channels.find(ch => ch.name === 'member-log'); // Do nothing if the channel wasn't found on this server if (!channel) return; // Send the message, mentioning the member From 2dcdc798ace9bace7fb6800a245f0f3802d0477d Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 2 Feb 2019 10:42:13 +0100 Subject: [PATCH 0950/1359] typings: add missing ImageSize numbers (#3045) To match the JS typedef: https://discord.js.org/#/docs/main/master/typedef/ImageURLOptions --- typings/index.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index a97225b7f..d6b95ba60 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1846,7 +1846,10 @@ declare module 'discord.js' { | 'jpg' | 'gif'; - type ImageSize = 128 + type ImageSize = 16 + | 32 + | 64 + | 128 | 256 | 512 | 1024 From f2ed93c08aba970a94671a0fec819035ad989195 Mon Sep 17 00:00:00 2001 From: Marcel Menzel Date: Sat, 2 Feb 2019 11:28:45 +0100 Subject: [PATCH 0951/1359] fix(WebSocketShard): report correct resumed event count (#3019) This PR attempts to fix the reported resumed event count in the debug output (where it is always displayed only as 1 event replayed) and in the emitted `resumed` event, where it passed the current sequence instead of passing the actual replayed event count (which was an utopic high number for smaller bots on resume). --- src/client/websocket/WebSocketShard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index ea4595b41..1cf0ef1c6 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -49,7 +49,7 @@ class WebSocketShard extends EventEmitter { * @type {number} * @private */ - this.closeSequence = 0; + this.closeSequence = oldShard ? oldShard.closeSequence : 0; /** * The current session id of the WebSocket @@ -223,7 +223,7 @@ class WebSocketShard extends EventEmitter { case WSEvents.RESUMED: { this.trace = packet.d._trace; this.status = Status.READY; - const replayed = packet.s - this.sequence; + const replayed = packet.s - this.closeSequence; this.debug(`RESUMED ${this.trace.join(' -> ')} | replayed ${replayed} events.`); this.heartbeat(); break; From db3ae0159bf2d8a44e7ecda4b9835a08b9210e8b Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 2 Feb 2019 20:28:24 +0100 Subject: [PATCH 0952/1359] docs(Structures): note about extending prior to instantiating client (#2884) --- src/util/Structures.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/Structures.js b/src/util/Structures.js index 5bdf5c86a..a742e2920 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -20,6 +20,8 @@ class Structures { /** * Extends a structure. + * Make sure to extend all structures before instantiating your client. + * Extending after doing so may not work as expected. * @param {string} structure Name of the structure class to extend * @param {Function} extender Function that takes the base class to extend as its only parameter and returns the * extended class/prototype From 1db78994dde894e796934758c7701e7bb160af00 Mon Sep 17 00:00:00 2001 From: "Rattmann (fallen)" <8607699+PlayTheFallen@users.noreply.github.com> Date: Sat, 2 Feb 2019 19:29:10 +0000 Subject: [PATCH 0953/1359] feat: MessageEmbed#length (#3003) * add MessageEmbed#length * update typings (+MessageEmbed#length) * eslint: L181 (max line length), L183 (missing semi) * eslint: L181 (trailing space) --- src/structures/MessageEmbed.js | 14 ++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 15 insertions(+) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 213a27a1d..0c925d276 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -169,6 +169,20 @@ class MessageEmbed { return this.color ? `#${this.color.toString(16).padStart(6, '0')}` : null; } + /** + * The accumulated length for the embed title, description, fields and footer text + * @type {number} + * @readonly + */ + get length() { + return ( + (this.title ? this.title.length : 0) + + (this.description ? this.description.length : 0) + + (this.fields.length >= 1 ? this.fields.reduce((prev, curr) => + prev + curr.name.length + curr.value.length, 0) : 0) + + (this.footer ? this.footer.text.length : 0)); + } + /** * Adds a field to the embed (max 25). * @param {StringResolvable} name The name of the field diff --git a/typings/index.d.ts b/typings/index.d.ts index d6b95ba60..b167be1f6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -729,6 +729,7 @@ declare module 'discord.js' { public footer: { text?: string; iconURL?: string; proxyIconURL?: string }; public readonly hexColor: string; public image: { url: string; proxyURL?: string; height?: number; width?: number; }; + public readonly length: number; public provider: { name: string; url: string; }; public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; }; public timestamp: number; From 75e264da5744b7258d92865856f5f238947c1f47 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sat, 2 Feb 2019 13:29:47 -0600 Subject: [PATCH 0954/1359] feat: Presence#clientStatus (#2997) * feat: Presence#clientStatus * fix Presence#equals check * fix typings * vlad changes * presence consistency docs * fix docs * fix big docs fail --- src/structures/ClientUser.js | 6 ++--- src/structures/Presence.js | 45 ++++++++++++++++++++++++++++-------- typings/index.d.ts | 19 +++++++++++---- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 79d7ad0e1..3ec32e9dd 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -80,7 +80,7 @@ class ClientUser extends Structures.get('User') { /** * Data resembling a raw Discord presence. * @typedef {Object} PresenceData - * @property {PresenceStatus} [status] Status of the user + * @property {PresenceStatusData} [status] Status of the user * @property {boolean} [afk] Whether the user is AFK * @property {Object} [activity] Activity the user is playing * @property {Object|string} [activity.application] An application object or application id @@ -111,12 +111,12 @@ class ClientUser extends Structures.get('User') { * * `idle` * * `invisible` * * `dnd` (do not disturb) - * @typedef {string} PresenceStatus + * @typedef {string} PresenceStatusData */ /** * Sets the status of the client user. - * @param {PresenceStatus} status Status to change to + * @param {PresenceStatusData} status Status to change to * @param {?number|number[]} [shardID] Shard ID(s) to have the activity set on * @returns {Promise} * @example diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 122b461d5..baacab138 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -11,14 +11,34 @@ const { ActivityTypes } = require('../util/Constants'); * @property {number} [type] Type of activity sent */ +/** + * The status of this presence: + * + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`offline`** - user is offline or invisible + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} PresenceStatus + */ + /** * Represents a user's presence. */ class Presence { constructor(client, data = {}) { Object.defineProperty(this, 'client', { value: client }); + /** + * The user ID of this presence + * @type {Snowflake} + */ this.userID = data.user.id; + + /** + * The guild of this presence + * @type {?Guild} + */ this.guild = data.guild; + this.patch(data); } @@ -40,23 +60,27 @@ class Presence { patch(data) { /** - * The status of the presence: - * - * * **`online`** - user is online - * * **`offline`** - user is offline or invisible - * * **`idle`** - user is AFK - * * **`dnd`** - user is in Do Not Disturb - * @type {string} + * The status of this presence + * @type {PresenceStatus} */ this.status = data.status || this.status || 'offline'; const activity = data.game || data.activity; /** - * The activity of the presence + * The activity of this presence * @type {?Activity} */ this.activity = activity ? new Activity(this, activity) : null; + /** + * The devices this presence is on + * @type {?object} + * @property {PresenceStatus} web + * @property {PresenceStatus} mobile + * @property {PresenceStatus} desktop + */ + this.clientStatus = data.client_status || null; + return this; } @@ -75,7 +99,10 @@ class Presence { return this === presence || ( presence && this.status === presence.status && - this.activity ? this.activity.equals(presence.activity) : !presence.activity + this.activity ? this.activity.equals(presence.activity) : !presence.activity && + this.clientStatus.web === presence.clientStatus.web && + this.clientStatus.mobile === presence.clientStatus.mobile && + this.clientStatus.desktop === presence.clientStatus.desktop ); } diff --git a/typings/index.d.ts b/typings/index.d.ts index b167be1f6..4910e3545 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -270,7 +270,7 @@ declare module 'discord.js' { public setAFK(afk: boolean): Promise; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; public setPresence(data: PresenceData): Promise; - public setStatus(status: PresenceStatus, shardID?: number | number[]): Promise; + public setStatus(status: PresenceStatusData, shardID?: number | number[]): Promise; public setUsername(username: string): Promise; } @@ -820,7 +820,8 @@ declare module 'discord.js' { constructor(client: Client, data?: object); public activity: Activity; public flags: Readonly; - public status: 'online' | 'offline' | 'idle' | 'dnd'; + public status: PresenceStatus; + public clientStatus: ClientPresenceStatusData; public readonly user: User; public readonly member?: GuildMember; public equals(presence: Presence): boolean; @@ -1994,7 +1995,7 @@ declare module 'discord.js' { }; type PresenceData = { - status?: PresenceStatus; + status?: PresenceStatusData; afk?: boolean; activity?: { name?: string; @@ -2006,7 +2007,17 @@ declare module 'discord.js' { type PresenceResolvable = Presence | UserResolvable | Snowflake; - type PresenceStatus = 'online' | 'idle' | 'invisible' | 'dnd'; + type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; + + type ClientPresenceStatusData = { + web?: ClientPresenceStatus, + mobile?: ClientPresenceStatus, + desktop?: ClientPresenceStatus + }; + + type PresenceStatus = ClientPresenceStatus | 'offline'; + + type PresenceStatusData = ClientPresenceStatus | 'invisible'; type RateLimitData = { timeout: number; From dd8ba00af4e4e4f64526cf476467fb0ded0257ff Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 4 Feb 2019 16:44:21 +0100 Subject: [PATCH 0955/1359] misc(index): export HTTPError class (#3051) --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index 1747f68c6..d9b8cb451 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ module.exports = { DataResolver: require('./util/DataResolver'), DataStore: require('./stores/DataStore'), DiscordAPIError: require('./rest/DiscordAPIError'), + HTTPError: require('./rest/HTTPError'), Permissions: require('./util/Permissions'), Speaking: require('./util/Speaking'), Snowflake: require('./util/Snowflake'), From d98d464d748a75e242b6e95a1308235bf4016e1b Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 4 Feb 2019 17:57:13 +0100 Subject: [PATCH 0956/1359] typings(GuildCreateChannelOptions): added missing properties (#3052) * typings(GuildCreateChannelOptions): Added missing properties * typings: Added `GuildChannelCloneOptions` --- typings/index.d.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4910e3545..a2607c18a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,7 +523,7 @@ declare module 'discord.js' { public readonly permissionsLocked: boolean; public readonly position: number; public rawPosition: number; - public clone(options?: GuildCreateChannelOptions): Promise; + public clone(options?: GuildChannelCloneOptions): Promise; public createInvite(options?: InviteOptions): Promise; public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; public edit(data: ChannelData, reason?: string): Promise; @@ -1777,15 +1777,20 @@ declare module 'discord.js' { type GuildChannelResolvable = Snowflake | GuildChannel; type GuildCreateChannelOptions = { - type?: 'text' | 'voice' | 'category' + permissionOverwrites?: OverwriteResolvable[] | Collection; + topic?: string; + type?: 'text' | 'voice' | 'category'; nsfw?: boolean; + parent?: ChannelResolvable; bitrate?: number; userLimit?: number; - parent?: ChannelResolvable; - permissionOverwrites?: OverwriteResolvable[] | Collection; rateLimitPerUser?: number; position?: number; - reason?: string + reason?: string; + }; + + type GuildChannelCloneOptions = GuildCreateChannelOptions & { + name?: string; }; type GuildEmojiCreateOptions = { From f826c9c75e976a8dca3fb4c88c130a1e12147a80 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 5 Feb 2019 10:26:16 +0000 Subject: [PATCH 0957/1359] voice: workaround for receiving audio (#2929 and discordapp/discord-api-docs#808) --- src/client/voice/VoiceConnection.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 078781b53..0242d541d 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -10,6 +10,15 @@ const EventEmitter = require('events'); const { Error } = require('../../errors'); const PlayInterface = require('./util/PlayInterface'); const Speaking = require('../../util/Speaking'); +const Silence = require('./util/Silence'); + +// Workaround for Discord now requiring silence to be sent before being able to receive audio +class SingleSilence extends Silence { + _read() { + super._read(); + this.push(null); + } +} const SUPPORTED_MODES = [ 'xsalsa20_poly1305_lite', @@ -419,13 +428,16 @@ class VoiceConnection extends EventEmitter { onSessionDescription(data) { Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; - clearTimeout(this.connectTimeout); - /** - * Emitted once the connection is ready, when a promise to join a voice channel resolves, - * the connection will already be ready. - * @event VoiceConnection#ready - */ - this.emit('ready'); + const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); + dispatcher.on('finish', () => { + clearTimeout(this.connectTimeout); + /** + * Emitted once the connection is ready, when a promise to join a voice channel resolves, + * the connection will already be ready. + * @event VoiceConnection#ready + */ + this.emit('ready'); + }); } /** From ae7269088b99dfff614ea66ba9c67baeb1bd9169 Mon Sep 17 00:00:00 2001 From: Kyra Date: Wed, 6 Feb 2019 18:15:05 +0100 Subject: [PATCH 0958/1359] typings(ShardClientUtil): fix `id` property type (#3054) Ref: https://github.com/discordjs/discord.js/blob/d98d464d748a75e242b6e95a1308235bf4016e1b/src/sharding/ShardClientUtil.js#L50 --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index a2607c18a..56f941075 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -951,7 +951,7 @@ declare module 'discord.js' { public client: Client; public readonly count: number; - public readonly id: number; + public readonly id: number | number[]; public mode: ShardingManagerMode; public parentPort: any; public broadcastEval(script: string): Promise; From c4325911130f30072246dd1b2e381c91293b3e6a Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 9 Feb 2019 16:07:31 +0100 Subject: [PATCH 0959/1359] feat(RoleStore, ChannelStore): `fetch()` method (#3071) * feat({Role,Channel}Store): fetch method * docs: Add usage examples to the new methods * misc: Add note of why we are fetching all roles even for a single one --- src/stores/ChannelStore.js | 18 ++++++++++++++++++ src/stores/RoleStore.js | 28 ++++++++++++++++++++++++++++ typings/index.d.ts | 3 +++ 3 files changed, 49 insertions(+) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 8d491c837..9ce6465cf 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -74,6 +74,24 @@ class ChannelStore extends DataStore { super.remove(id); } + /** + * Obtains a channel from Discord, or the channel cache if it's already available. + * @param {Snowflake} id ID of the channel + * @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already + * @returns {Promise} + * @example + * // Fetch a channel by its id + * client.channels.fetch('222109930545610754') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ + fetch(id, cache = true) { + const existing = this.get(id); + if (existing) return Promise.resolve(existing); + + return this.client.api.channels(id).get().then(data => this.add(data, null, cache)); + } + /** * Data that can be resolved to give a Channel object. This can be: * * A Channel object diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 14956c309..6f97d2770 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -19,6 +19,34 @@ class RoleStore extends DataStore { return super.add(data, cache, { extras: [this.guild] }); } + /** + * Obtains one or more roles from Discord, or the role cache if they're already available. + * @param {Snowflake} [id] ID or IDs of the role(s) + * @param {boolean} [cache=true] Whether to cache the new roles objects if it weren't already + * @returns {Promise} + * @example + * // Fetch all roles from the guild + * message.guild.roles.fetch() + * .then(roles => console.log(`There are ${roles.size} roles.`)) + * .catch(console.error); + * @example + * // Fetch a single role + * message.guild.roles.fetch('222078108977594368') + * .then(role => console.log(`The role color is: ${role.color}`)) + * .catch(console.error); + */ + async fetch(id, cache = true) { + if (id) { + const existing = this.get(id); + if (existing) return existing; + } + + // We cannot fetch a single role, as of this commit's date, Discord API throws with 405 + const roles = await this.client.api.guilds(this.guild.id).roles.get(); + for (const role of roles) this.add(role, cache); + return id ? this.get(id) || null : this; + } + /** * Data that can be resolved to a Role object. This can be: * * A Role diff --git a/typings/index.d.ts b/typings/index.d.ts index 56f941075..60a19f3db 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1320,6 +1320,7 @@ declare module 'discord.js' { export class ChannelStore extends DataStore { constructor(client: Client, iterable: Iterable, options?: { lru: boolean }); constructor(client: Client, options?: { lru: boolean }); + public fetch(id: Snowflake, cache?: boolean): Promise; } export class DataStore, R = any> extends Collection { @@ -1410,6 +1411,8 @@ declare module 'discord.js' { public readonly highest: Role; public create(options?: { data?: RoleData, reason?: string }): Promise; + public fetch(id?: Snowflake, cache?: boolean): Promise; + public fetch(id: Snowflake, cache?: boolean): Promise; } export class UserStore extends DataStore { From 9449f8df8f3c65b350ad7844376651f7fd06ba52 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sat, 9 Feb 2019 18:20:10 +0100 Subject: [PATCH 0960/1359] cleanup(GuildMember{RoleStore}): Rewrite to async/await (#3072) - Fixed a bug where `GuildMemberRoleStore#{add,remove}` would create a rejected promise inside a promise, instead of rejecting the first one. - Fixed a bug where `GuildMember#edit` with a specified unknown channel would throw synchronously, instead of rejecting asynchronously. --- src/stores/GuildMemberRoleStore.js | 16 ++++++++-------- src/structures/GuildMember.js | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 8789df212..9ab340d68 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -67,8 +67,8 @@ class GuildMemberRoleStore extends Collection { if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } const newRoles = [...new Set(roleOrRoles.concat(...this.values()))]; @@ -76,8 +76,8 @@ class GuildMemberRoleStore extends Collection { } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); if (roleOrRoles === null) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason }); @@ -98,8 +98,8 @@ class GuildMemberRoleStore extends Collection { if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } const newRoles = this.filter(role => !roleOrRoles.includes(role)); @@ -107,8 +107,8 @@ class GuildMemberRoleStore extends Collection { } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); if (roleOrRoles === null) { - return Promise.reject(new TypeError('INVALID_TYPE', 'roles', - 'Array or Collection of Roles or Snowflakes', true)); + throw new TypeError('INVALID_TYPE', 'roles', + 'Array or Collection of Roles or Snowflakes', true); } await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].delete({ reason }); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d91071c93..73adb69ac 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -248,7 +248,7 @@ class GuildMember extends Base { * @param {string} [reason] Reason for editing this user * @returns {Promise} */ - edit(data, reason) { + async edit(data, reason) { if (data.channel) { data.channel = this.guild.channels.resolve(data.channel); if (!data.channel || data.channel.type !== 'voice') { @@ -266,12 +266,12 @@ class GuildMember extends Base { } else { endpoint = endpoint.members(this.id); } - return endpoint.patch({ data, reason }).then(() => { - const clone = this._clone(); - data.user = this.user; - clone._patch(data); - return clone; - }); + await endpoint.patch({ data, reason }); + + const clone = this._clone(); + data.user = this.user; + clone._patch(data); + return clone; } /** From d057a5040cc372946a341c0b211cd93ea1beb299 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sun, 10 Feb 2019 00:51:23 +0100 Subject: [PATCH 0961/1359] ci: Test in Node.js 10 and 11, deploy with Node.js 10 (#3069) --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3b89d2afa..2215629ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: node_js node_js: - - 8 - - 9 + - 10 + - 11 install: npm install script: bash ./travis/test.sh jobs: include: - stage: deploy - node_js: 9 + node_js: 10 script: bash ./travis/deploy.sh env: - ENCRYPTION_LABEL="af862fa96d3e" From ff95e587cb5a49921eb206dd2cd034d692e99bff Mon Sep 17 00:00:00 2001 From: Atlas <41349879+cloudrex@users.noreply.github.com> Date: Sat, 9 Feb 2019 18:53:47 -0500 Subject: [PATCH 0962/1359] Better wording (#3032) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c4cb7c5ec..faaa2bfd1 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ ## About -discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to interact with the -[Discord API](https://discordapp.com/developers/docs/intro) very easily. +discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the +[Discord API](https://discordapp.com/developers/docs/intro). - Object-oriented - Predictable abstractions From 7324a993edf0c92c94ff70f3e5462535bfa08455 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 10 Feb 2019 16:21:59 +0100 Subject: [PATCH 0963/1359] fix: ensure VIEW_CHANNEL permissions before trying to join (#3046) * fix: ensure VIEW_CHANNEL permissions before joining * nit(GuildChannel): remove the redundant truthy check --- src/structures/GuildChannel.js | 13 ++++++++++++- src/structures/VoiceChannel.js | 7 ++++--- typings/index.d.ts | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index dc0a4e415..9904e4c14 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -523,10 +523,21 @@ class GuildChannel extends Channel { * @readonly */ get manageable() { + if (this.client.user.id === this.guild.ownerID) return true; + if (!this.viewable) return false; + return this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false); + } + + /** + * Whether the channel is viewable by the client user + * @type {boolean} + * @readonly + */ + get viewable() { if (this.client.user.id === this.guild.ownerID) return true; const permissions = this.permissionsFor(this.client.user); if (!permissions) return false; - return permissions.has([Permissions.FLAGS.MANAGE_CHANNELS, Permissions.FLAGS.VIEW_CHANNEL], false); + return permissions.has(Permissions.FLAGS.VIEW_CHANNEL, false); } /** diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index cf9b1ee99..2d1fa956e 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -71,14 +71,15 @@ class VoiceChannel extends GuildChannel { } /** - * Checks if the client has permission join the voice channel + * Whether the channel is joinable by the client user * @type {boolean} * @readonly */ get joinable() { if (browser) return false; - if (!this.permissionsFor(this.client.user).has('CONNECT', false)) return false; - if (this.full && !this.permissionsFor(this.client.user).has('MOVE_MEMBERS', false)) return false; + if (!this.viewable) return false; + if (!this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false)) return false; + if (this.full && !this.permissionsFor(this.client.user).has(Permissions.FLAGS.MOVE_MEMBERS, false)) return false; return true; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 60a19f3db..dccd33049 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -523,6 +523,7 @@ declare module 'discord.js' { public readonly permissionsLocked: boolean; public readonly position: number; public rawPosition: number; + public readonly viewable: boolean; public clone(options?: GuildChannelCloneOptions): Promise; public createInvite(options?: InviteOptions): Promise; public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; From 793341dbb401f2956d60fd6b9f94a611cf539126 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 10 Feb 2019 18:28:03 +0200 Subject: [PATCH 0964/1359] fix: Sharding issues, silent disconnects and code cleanup (#2976) * fix: Sharding bugs, silent disconnects and cleanup code * typings * fix: Destroy connecting with close code different from 1000 Per `If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume.` * misc: Wait x ms before reconnecting Per https://discordapp.com/developers/docs/topics/gateway#resuming * docs * nit: docs * misc: Prevent multiple calls to WebSocketManager#destroy * fix: Implement destroying if you reset the token * misc: Clear the WS packet queue on WebSocketShard#destroy You can't send those packets anywhere anymore, so no point in keeping them * fix: Handle session limits when reconnecting a full shard, cleanup * misc: No need to create a new shard instance * fix: closeSequence being null, thus emitting null on Client#resumed * misc: Remove GUILD_SYNC Gateway handler and add missing dot to string * misc: Close WS with code 4000 if we didn't get a heartbeat in time As said in the Discord API server * fix: Handle ready emitting in onPacket Doesn't allow broken packets * misc: Close the connection if Discord asks for a reconnect Prevents double triggers * testing: Prevent multiple reconnect attempts on a shard Should fix some issues some people have had. * fix: Prevent multiple reconnect calls on the shard, re-use conn to identify, remove reconnect function Note: Closing the WS with 1000 makes the session invalid * misc: Forgot to remove 2 unneeded setters * docs: Wrong param docstring for WebSocketShard#destroy * misc: Set status to reconnecting after destroying * misc: Close connection with code 1000 on session invalidated Allows us to cleanup the shard and do a full reconnect Also remove identify wait delay, not used anywhere * fix: Fix zlib crash on node And with that, the PR is done! * misc: Implement a reconnect queue And that is all there was to be done in this PR. Shards now queue up for a reconnect * nit: Debug the queue after destroying * docs: Make the invalidated event clearer * lint: I'm good at my job * docs * docs: Make description for isReconnectingShards accurate *can I stop finding issues, this PR is meant to be done* * misc: Remove shard from bind params * misc: Code re-ordering and cleanup Resumes do not need to be queued up, as they do not count to the identify limit, and after some testing, they don't have the 5 second delay required, like in identify * fix: Issues with token regeneration and shards not properly handling them We close the ws connection with code 1000 if we get an invalid session payload, that way we can queue the reconnects and handle any issues * misc: Remove useless delays on session invalidated They get handled by the rest of the code already * lint * misc: reset the sequence on Shard#destroy This especially is a problem if you need to re-identify, as the sequence doesn't get set to the current one, causing the sequence to be wrong * fix: GitHub rebase and minor tweak * Implement a 15 second timeout if shards don't connect till then Should prevent shards that never reconnect * revert: Make WebSocketShard#send and WebSocketManager#broadcast public * typings: Set type to void instead of undefined * docs: Requested Changes --- src/client/websocket/WebSocketManager.js | 217 ++++++---- src/client/websocket/WebSocketShard.js | 416 ++++++++++---------- src/client/websocket/handlers/GUILD_SYNC.js | 5 - typings/index.d.ts | 12 +- 4 files changed, 353 insertions(+), 297 deletions(-) delete mode 100644 src/client/websocket/handlers/GUILD_SYNC.js diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index fd7499682..bc34b84b3 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,6 +1,7 @@ 'use strict'; const Collection = require('../../util/Collection'); +const Util = require('../../util/Util'); const WebSocketShard = require('./WebSocketShard'); const { Events, Status, WSEvents } = require('../../util/Constants'); const PacketHandlers = require('./handlers'); @@ -16,7 +17,7 @@ const BeforeReadyWhitelist = [ ]; /** - * WebSocket Manager of the client. + * The WebSocket manager for this client. */ class WebSocketManager { constructor(client) { @@ -28,52 +29,60 @@ class WebSocketManager { Object.defineProperty(this, 'client', { value: client }); /** - * The gateway this WebSocketManager uses. + * The gateway this manager uses * @type {?string} */ this.gateway = undefined; /** - * An array of shards spawned by this WebSocketManager. + * A collection of all shards this manager handles * @type {Collection} */ this.shards = new Collection(); /** - * An array of queued shards to be spawned by this WebSocketManager. - * @type {Array} + * An array of shards to be spawned or reconnected + * @type {Array} * @private */ - this.spawnQueue = []; + this.shardQueue = []; /** - * Whether or not this WebSocketManager is currently spawning shards. - * @type {boolean} - * @private - */ - this.spawning = false; - - /** - * An array of queued events before this WebSocketManager became ready. + * An array of queued events before this WebSocketManager became ready * @type {object[]} * @private */ this.packetQueue = []; /** - * The current status of this WebSocketManager. + * The current status of this WebSocketManager * @type {number} */ this.status = Status.IDLE; /** - * The current session limit of the client. + * If this manager is expected to close + * @type {boolean} + * @private + */ + this.expectingClose = false; + + /** + * The current session limit of the client * @type {?Object} + * @private * @prop {number} total Total number of identifies available * @prop {number} remaining Number of identifies remaining * @prop {number} reset_after Number of milliseconds after which the limit resets */ this.sessionStartLimit = null; + + /** + * If the manager is currently reconnecting shards + * @type {boolean} + * @private + */ + this.isReconnectingShards = false; } /** @@ -89,103 +98,147 @@ class WebSocketManager { /** * Emits a debug event. * @param {string} message Debug message - * @returns {void} * @private */ debug(message) { - this.client.emit(Events.DEBUG, `[connection] ${message}`); + this.client.emit(Events.DEBUG, message); } /** - * Handles the session identify rate limit for a shard. - * @param {WebSocketShard} shard Shard to handle + * Checks if a new identify payload can be sent. * @private + * @returns {Promise} */ - async _handleSessionLimit(shard) { + async _checkSessionLimit() { this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit); const { remaining, reset_after } = this.sessionStartLimit; - if (remaining !== 0) { - this.spawn(); - } else { - shard.debug(`Exceeded identify threshold, setting a timeout for ${reset_after} ms`); - setTimeout(() => this.spawn(), this.sessionStartLimit.reset_after); - } + if (remaining !== 0) return true; + return reset_after; } /** - * Used to spawn WebSocketShards. - * @param {?WebSocketShard|WebSocketShard[]|number|string} query The WebSocketShards to be spawned - * @returns {void} + * Handles the session identify rate limit for creating a shard. * @private */ - spawn(query) { - if (query !== undefined) { - if (Array.isArray(query)) { - for (const item of query) { - if (!this.spawnQueue.includes(item)) this.spawnQueue.push(item); - } - } else if (!this.spawnQueue.includes(query)) { - this.spawnQueue.push(query); - } - } - - if (this.spawning || !this.spawnQueue.length) return; - - this.spawning = true; - let item = this.spawnQueue.shift(); - - if (typeof item === 'string' && !isNaN(item)) item = Number(item); - if (typeof item === 'number') { - const shard = new WebSocketShard(this, item, this.shards.get(item)); - this.shards.set(item, shard); - shard.once(Events.READY, () => { - this.spawning = false; - this.client.setTimeout(() => this._handleSessionLimit(shard), 5000); - }); - shard.once(Events.INVALIDATED, () => { - this.spawning = false; - }); - } else if (item instanceof WebSocketShard) { - item.reconnect(); + async _handleSessionLimit() { + const canSpawn = await this._checkSessionLimit(); + if (typeof canSpawn === 'number') { + this.debug(`Exceeded identify threshold, setting a timeout for ${canSpawn}ms`); + await Util.delayFor(canSpawn); } + this.create(); } /** * Creates a connection to a gateway. * @param {string} [gateway=this.gateway] The gateway to connect to - * @returns {void} * @private */ connect(gateway = this.gateway) { this.gateway = gateway; if (typeof this.client.options.shards === 'number') { - this.debug('Spawning 1 shard'); - this.spawn(this.client.options.shards); + this.debug(`Spawning shard with ID ${this.client.options.shards}`); + this.shardQueue.push(this.client.options.shards); } else if (Array.isArray(this.client.options.shards)) { this.debug(`Spawning ${this.client.options.shards.length} shards`); - for (const shard of this.client.options.shards) { - this.spawn(shard); - } + this.shardQueue.push(...this.client.options.shards); } else { this.debug(`Spawning ${this.client.options.shardCount} shards`); - for (let i = 0; i < this.client.options.shardCount; i++) { - this.spawn(i); + this.shardQueue.push(...Array.from({ length: this.client.options.shardCount }, (_, index) => index)); + } + this.create(); + } + + /** + * Creates or reconnects a shard. + * @private + */ + create() { + // Nothing to create + if (!this.shardQueue.length) return; + + let item = this.shardQueue.shift(); + if (typeof item === 'string' && !isNaN(item)) item = Number(item); + + if (item instanceof WebSocketShard) { + const timeout = setTimeout(() => { + this.debug(`[Shard ${item.id}] Failed to connect in 15s... Destroying and trying again`); + item.destroy(); + if (!this.shardQueue.includes(item)) this.shardQueue.push(item); + this.reconnect(true); + }, 15000); + item.once(Events.READY, this._shardReady.bind(this, timeout)); + item.once(Events.RESUMED, this._shardReady.bind(this, timeout)); + item.connect(); + return; + } + + const shard = new WebSocketShard(this, item); + this.shards.set(item, shard); + shard.once(Events.READY, this._shardReady.bind(this)); + } + + /** + * Shared handler for shards turning ready or resuming. + * @param {Timeout} [timeout=null] Optional timeout to clear if shard didn't turn ready in time + * @private + */ + _shardReady(timeout = null) { + if (timeout) clearTimeout(timeout); + if (this.shardQueue.length) { + this.client.setTimeout(this._handleSessionLimit.bind(this), 5000); + } else { + this.isReconnectingShards = false; + } + } + + /** + * Handles the reconnect of a shard. + * @param {WebSocketShard|boolean} shard The shard to reconnect, or a boolean to indicate an immediate reconnect + * @private + */ + async reconnect(shard) { + // If the item is a shard, add it to the queue + if (shard instanceof WebSocketShard) this.shardQueue.push(shard); + if (typeof shard === 'boolean') { + // If a boolean is passed, force a reconnect right now + } else if (this.isReconnectingShards) { + // If we're already reconnecting shards, and no boolean was provided, return + return; + } + this.isReconnectingShards = true; + try { + await this._handleSessionLimit(); + } catch (error) { + // If we get an error at this point, it means we cannot reconnect anymore + if (this.client.listenerCount(Events.INVALIDATED)) { + /** + * Emitted when the client's session becomes invalidated. + * You are expected to handle closing the process gracefully and preventing a boot loop + * if you are listening to this event. + * @event Client#invalidated + */ + this.client.emit(Events.INVALIDATED); + // Destroy just the shards. This means you have to handle the cleanup yourself + this.destroy(); + } else { + this.client.destroy(); } } } /** * Processes a packet and queues it if this WebSocketManager is not ready. - * @param {Object} packet The packet to be handled - * @param {WebSocketShard} shard The shard that will handle this packet + * @param {Object} [packet] The packet to be handled + * @param {WebSocketShard} [shard] The shard that will handle this packet * @returns {boolean} * @private */ handlePacket(packet, shard) { if (packet && this.status !== Status.READY) { if (!BeforeReadyWhitelist.includes(packet.t)) { - this.packetQueue.push({ packet, shardID: shard.id }); + this.packetQueue.push({ packet, shard }); return false; } } @@ -193,7 +246,7 @@ class WebSocketManager { if (this.packetQueue.length) { const item = this.packetQueue.shift(); this.client.setImmediate(() => { - this.handlePacket(item.packet, this.shards.get(item.shardID)); + this.handlePacket(item.packet, item.shard); }); } @@ -201,7 +254,7 @@ class WebSocketManager { PacketHandlers[packet.t](this.client, packet, shard); } - return false; + return true; } /** @@ -211,7 +264,7 @@ class WebSocketManager { */ checkReady() { if (this.shards.size !== this.client.options.shardCount || - this.shards.some(s => s && s.status !== Status.READY)) { + this.shards.some(s => s.status !== Status.READY)) { return false; } @@ -258,26 +311,22 @@ class WebSocketManager { /** * Broadcasts a message to every shard in this WebSocketManager. * @param {*} packet The packet to send + * @private */ broadcast(packet) { - for (const shard of this.shards.values()) { - shard.send(packet); - } + for (const shard of this.shards.values()) shard.send(packet); } /** * Destroys all shards. - * @returns {void} * @private */ destroy() { - this.gateway = undefined; - // Lock calls to spawn - this.spawning = true; - - for (const shard of this.shards.values()) { - shard.destroy(); - } + if (this.expectingClose) return; + this.expectingClose = true; + this.isReconnectingShards = false; + this.shardQueue.length = 0; + for (const shard of this.shards.values()) shard.destroy(); } } diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1cf0ef1c6..96ab06a44 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -4,6 +4,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); const Util = require('../../util/Util'); + let zlib; try { zlib = require('zlib-sync'); @@ -13,10 +14,10 @@ try { } /** - * Represents a Shard's Websocket connection. + * Represents a Shard's WebSocket connection */ class WebSocketShard extends EventEmitter { - constructor(manager, id, oldShard) { + constructor(manager, id) { super(); /** @@ -26,7 +27,7 @@ class WebSocketShard extends EventEmitter { this.manager = manager; /** - * The id of the this shard. + * The ID of the this shard * @type {number} */ this.id = id; @@ -38,28 +39,28 @@ class WebSocketShard extends EventEmitter { this.status = Status.IDLE; /** - * The current sequence of the WebSocket + * The current sequence of the shard * @type {number} * @private */ - this.sequence = oldShard ? oldShard.sequence : -1; + this.sequence = -1; /** - * The sequence on WebSocket close + * The sequence of the shard after close * @type {number} * @private */ - this.closeSequence = oldShard ? oldShard.closeSequence : 0; + this.closeSequence = 0; /** - * The current session id of the WebSocket - * @type {?string} + * The current session ID of the shard + * @type {string} * @private */ - this.sessionID = oldShard && oldShard.sessionID; + this.sessionID = undefined; /** - * Previous heartbeat pings of the websocket (most recent first, limited to three elements) + * The previous 3 heartbeat pings of the shard (most recent first) * @type {number[]} */ this.pings = []; @@ -71,6 +72,13 @@ class WebSocketShard extends EventEmitter { */ this.lastPingTimestamp = -1; + /** + * If we received a heartbeat ack back. Used to identify zombie connections + * @type {boolean} + * @private + */ + this.lastHeartbeatAcked = true; + /** * List of servers the shard is connected to * @type {string[]} @@ -96,7 +104,7 @@ class WebSocketShard extends EventEmitter { * @type {?WebSocket} * @private */ - this.ws = null; + this.connection = null; /** * @external Inflate @@ -110,13 +118,7 @@ class WebSocketShard extends EventEmitter { */ this.inflate = null; - /** - * Whether or not the WebSocket is expected to be closed - * @type {boolean} - */ - this.expectingClose = false; - - this.connect(); + if (this.manager.gateway) this.connect(); } /** @@ -135,35 +137,42 @@ class WebSocketShard extends EventEmitter { * @private */ debug(message) { - this.manager.debug(`[shard ${this.id}] ${message}`); + this.manager.debug(`[Shard ${this.id}] ${message}`); } /** - * Sends a heartbeat or sets an interval for sending heartbeats. - * @param {number} [time] If -1, clears the interval, any other number sets an interval - * If no value is given, a heartbeat will be sent instantly + * Sends a heartbeat to the WebSocket. + * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect * @private */ - heartbeat(time) { - if (!isNaN(time)) { - if (time === -1) { + sendHeartbeat() { + if (!this.lastHeartbeatAcked) { + this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); + this.connection.close(4000); + return; + } + this.debug('Sending a heartbeat'); + this.lastHeartbeatAcked = false; + this.lastPingTimestamp = Date.now(); + this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }); + } + + /** + * Sets the heartbeat timer for this shard. + * @param {number} time If -1, clears the interval, any other number sets an interval + * @private + */ + setHeartbeatTimer(time) { + if (time === -1) { + if (this.heartbeatInterval) { this.debug('Clearing heartbeat interval'); this.manager.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; - } else { - this.debug(`Setting a heartbeat interval for ${time}ms`); - if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = this.manager.client.setInterval(() => this.heartbeat(), time); } return; } - - this.debug('Sending a heartbeat'); - this.lastPingTimestamp = Date.now(); - this.send({ - op: OPCodes.HEARTBEAT, - d: this.sequence, - }); + this.debug(`Setting a heartbeat interval for ${time}ms`); + this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); } /** @@ -171,6 +180,7 @@ class WebSocketShard extends EventEmitter { * @private */ ackHeartbeat() { + this.lastHeartbeatAcked = true; const latency = Date.now() - this.lastPingTimestamp; this.debug(`Heartbeat acknowledged, latency of ${latency}ms`); this.pings.unshift(latency); @@ -178,18 +188,19 @@ class WebSocketShard extends EventEmitter { } /** - * Connects the shard to a gateway. + * Connects this shard to the gateway. * @private */ connect() { + const { expectingClose, gateway } = this.manager; + if (expectingClose) return; this.inflate = new zlib.Inflate({ chunkSize: 65535, flush: zlib.Z_SYNC_FLUSH, to: WebSocket.encoding === 'json' ? 'string' : '', }); - const gateway = this.manager.gateway; this.debug(`Connecting to ${gateway}`); - const ws = this.ws = WebSocket.create(gateway, { + const ws = this.connection = WebSocket.create(gateway, { v: this.manager.client.options.ws.version, compress: 'zlib-stream', }); @@ -200,73 +211,12 @@ class WebSocketShard extends EventEmitter { this.status = Status.CONNECTING; } - /** - * Called whenever a packet is received - * @param {Object} packet Packet received - * @returns {any} - * @private - */ - onPacket(packet) { - if (!packet) { - this.debug('Received null packet'); - return false; - } - - switch (packet.t) { - case WSEvents.READY: - this.sessionID = packet.d.session_id; - this.trace = packet.d._trace; - this.status = Status.READY; - this.debug(`READY ${this.trace.join(' -> ')} ${this.sessionID}`); - this.heartbeat(); - break; - case WSEvents.RESUMED: { - this.trace = packet.d._trace; - this.status = Status.READY; - const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED ${this.trace.join(' -> ')} | replayed ${replayed} events.`); - this.heartbeat(); - break; - } - } - - if (packet.s > this.sequence) this.sequence = packet.s; - - switch (packet.op) { - case OPCodes.HELLO: - this.identify(); - return this.heartbeat(packet.d.heartbeat_interval); - case OPCodes.RECONNECT: - return this.reconnect(); - case OPCodes.INVALID_SESSION: - this.sequence = -1; - this.debug('Session invalidated'); - // If the session isn't resumable - if (!packet.d) { - // If we had a session ID before - if (this.sessionID) { - this.sessionID = null; - return this.identify(2500); - } - return this.identify(5000); - } - return this.identify(); - case OPCodes.HEARTBEAT_ACK: - return this.ackHeartbeat(); - case OPCodes.HEARTBEAT: - return this.heartbeat(); - default: - return this.manager.handlePacket(packet, this); - } - } - /** * Called whenever a connection is opened to the gateway. - * @param {Event} event Received open event * @private */ onOpen() { - this.debug('Connection open'); + this.debug('Connected to the gateway'); } /** @@ -293,87 +243,102 @@ class WebSocketShard extends EventEmitter { this.manager.client.emit(Events.ERROR, err); return; } - if (packet.t === WSEvents.READY) { - /** - * Emitted when a shard becomes ready - * @event WebSocketShard#ready - */ - this.emit(Events.READY); - - /** - * Emitted when a shard becomes ready - * @event Client#shardReady - * @param {number} shardID The id of the shard - */ - this.manager.client.emit(Events.SHARD_READY, this.id); - } this.onPacket(packet); } /** - * Called whenever an error occurs with the WebSocket. - * @param {Error} error The error that occurred + * Called whenever a packet is received. + * @param {Object} packet Packet received * @private */ - onError(error) { - if (error && error.message === 'uWs client connection error') { - this.reconnect(); + onPacket(packet) { + if (!packet) { + this.debug('Received null or broken packet'); return; } - this.emit(Events.INVALIDATED); - /** - * Emitted whenever the client's WebSocket encounters a connection error. - * @event Client#error - * @param {Error} error The encountered error - */ - this.manager.client.emit(Events.ERROR, error); - } + switch (packet.t) { + case WSEvents.READY: + /** + * Emitted when a shard becomes ready. + * @event WebSocketShard#ready + */ + this.emit(Events.READY); + /** + * Emitted when a shard becomes ready. + * @event Client#shardReady + * @param {number} shardID The ID of the shard + */ + this.manager.client.emit(Events.SHARD_READY, this.id); - /** - * @external CloseEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} - */ - - /** - * Called whenever a connection to the gateway is closed. - * @param {CloseEvent} event Close event that was received - * @returns {void} - * @private - */ - onClose(event) { - this.closeSequence = this.sequence; - this.emit('close', event); - if (event.code === 1000 ? this.expectingClose : WSCodes[event.code]) { - /** - * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. - * @event Client#disconnect - * @param {CloseEvent} event The WebSocket close event - * @param {number} shardID The shard that disconnected - */ - this.manager.client.emit(Events.DISCONNECT, event, this.id); - this.debug(WSCodes[event.code]); - this.heartbeat(-1); - return; + this.sessionID = packet.d.session_id; + this.trace = packet.d._trace; + this.status = Status.READY; + this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}`); + this.lastHeartbeatAcked = true; + this.sendHeartbeat(); + break; + case WSEvents.RESUMED: { + this.emit(Events.RESUMED); + this.trace = packet.d._trace; + this.status = Status.READY; + const replayed = packet.s - this.closeSequence; + this.debug(`RESUMED ${this.trace.join(' -> ')} | Replayed ${replayed} events.`); + this.lastHeartbeatAcked = true; + this.sendHeartbeat(); + break; + } + } + + if (packet.s > this.sequence) this.sequence = packet.s; + + switch (packet.op) { + case OPCodes.HELLO: + this.setHeartbeatTimer(packet.d.heartbeat_interval); + this.identify(); + break; + case OPCodes.RECONNECT: + this.connection.close(1001); + break; + case OPCodes.INVALID_SESSION: + this.debug(`Session was invalidated. Resumable: ${packet.d}.`); + // If the session isn't resumable + if (!packet.d) { + // Reset the sequence, since it isn't valid anymore + this.sequence = -1; + // If we had a session ID before + if (this.sessionID) { + this.sessionID = null; + this.connection.close(1000); + return; + } + this.connection.close(1000); + return; + } + this.identifyResume(); + break; + case OPCodes.HEARTBEAT_ACK: + this.ackHeartbeat(); + break; + case OPCodes.HEARTBEAT: + this.sendHeartbeat(); + break; + default: + this.manager.handlePacket(packet, this); } - this.expectingClose = false; - this.reconnect(Events.INVALIDATED, 5100); } /** * Identifies the client on a connection. - * @param {?number} [wait=0] Amount of time to wait before identifying * @returns {void} * @private */ - identify(wait = 0) { - if (wait) return this.manager.client.setTimeout(this.identify.bind(this), wait); + identify() { return this.sessionID ? this.identifyResume() : this.identifyNew(); } /** * Identifies as a new connection on the gateway. - * @returns {void} * @private */ identifyNew() { @@ -382,10 +347,11 @@ class WebSocketShard extends EventEmitter { return; } // Clone the generic payload and assign the token - const d = Object.assign({ token: this.manager.client.token }, this.manager.client.options.ws); - - const { totalShardCount } = this.manager.client.options; - d.shard = [this.id, Number(totalShardCount)]; + const d = { + ...this.manager.client.options.ws, + token: this.manager.client.token, + shard: [this.id, Number(this.manager.client.options.totalShardCount)], + }; // Send the payload this.debug('Identifying as a new session'); @@ -402,23 +368,90 @@ class WebSocketShard extends EventEmitter { this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead'); return this.identifyNew(); } - this.debug(`Attempting to resume session ${this.sessionID}`); + + this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`); const d = { token: this.manager.client.token, session_id: this.sessionID, - seq: this.sequence, + seq: this.closeSequence, }; - return this.send({ - op: OPCodes.RESUME, - d, - }); + return this.send({ op: OPCodes.RESUME, d }); + } + + /** + * Called whenever an error occurs with the WebSocket. + * @param {Error} error The error that occurred + * @private + */ + onError(error) { + if (error && error.message === 'uWs client connection error') { + this.connection.close(4000); + return; + } + + /** + * Emitted whenever the client's WebSocket encounters a connection error. + * @event Client#error + * @param {Error} error The encountered error + * @param {number} shardID The shard that encountered this error + */ + this.manager.client.emit(Events.ERROR, error, this.id); + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * Called whenever a connection to the gateway is closed. + * @param {CloseEvent} event Close event that was received + * @private + */ + onClose(event) { + this.closeSequence = this.sequence; + this.debug(`WebSocket was closed. + Event Code: ${event.code} + Reason: ${event.reason}`); + + if (event.code === 1000 ? this.manager.expectingClose : WSCodes[event.code]) { + /** + * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. + * @event Client#disconnect + * @param {CloseEvent} event The WebSocket close event + * @param {number} shardID The shard that disconnected + */ + this.manager.client.emit(Events.DISCONNECT, event, this.id); + this.debug(WSCodes[event.code]); + return; + } + + this.destroy(); + + this.status = Status.RECONNECTING; + + /** + * Emitted whenever a shard tries to reconnect to the WebSocket. + * @event Client#reconnecting + * @param {number} shardID The shard ID that is reconnecting + */ + this.manager.client.emit(Events.RECONNECTING, this.id); + + this.debug(`${this.sessionID ? `Reconnecting in 3500ms` : 'Queueing a reconnect'} to the gateway...`); + + if (this.sessionID) { + Util.delayFor(3500).then(() => this.connect()); + } else { + this.manager.reconnect(this); + } } /** * Adds data to the queue to be sent. * @param {Object} data Packet to send + * @private * @returns {void} */ send(data) { @@ -433,11 +466,12 @@ class WebSocketShard extends EventEmitter { * @private */ _send(data) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${data} but no WebSocket is available!`); + if (!this.connection || this.connection.readyState !== WebSocket.OPEN) { + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); return; } - this.ws.send(WebSocket.pack(data), err => { + + this.connection.send(WebSocket.pack(data), err => { if (err) this.manager.client.emit(Events.ERROR, err); }); } @@ -465,44 +499,22 @@ class WebSocketShard extends EventEmitter { } /** - * Triggers a shard reconnect. - * @param {?string} [event] The event for the shard to emit - * @param {?number} [reconnectIn] Time to wait before reconnecting - * @returns {Promise} - * @private - */ - async reconnect(event, reconnectIn) { - this.heartbeat(-1); - this.status = Status.RECONNECTING; - - /** - * Emitted whenever a shard tries to reconnect to the WebSocket. - * @event Client#reconnecting - */ - this.manager.client.emit(Events.RECONNECTING, this.id); - - if (event === Events.INVALIDATED) this.emit(event); - this.debug(reconnectIn ? `Reconnecting in ${reconnectIn}ms` : 'Reconnecting now'); - if (reconnectIn) await Util.delayFor(reconnectIn); - this.manager.spawn(this.id); - } - - /** - * Destroys the current shard and terminates its connection. - * @returns {void} + * Destroys this shard and closes its connection. * @private */ destroy() { - this.heartbeat(-1); - this.expectingClose = true; - if (this.ws) this.ws.close(1000); - this.ws = null; + this.setHeartbeatTimer(-1); + if (this.connection) this.connection.close(1000); + this.connection = null; this.status = Status.DISCONNECTED; this.ratelimit.remaining = this.ratelimit.total; + this.ratelimit.queue.length = 0; if (this.ratelimit.timer) { this.manager.client.clearTimeout(this.ratelimit.timer); this.ratelimit.timer = null; } + this.sequence = -1; } } + module.exports = WebSocketShard; diff --git a/src/client/websocket/handlers/GUILD_SYNC.js b/src/client/websocket/handlers/GUILD_SYNC.js deleted file mode 100644 index b7e7d1b24..000000000 --- a/src/client/websocket/handlers/GUILD_SYNC.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = (client, packet) => { - client.actions.GuildSync.handle(packet.d); -}; diff --git a/typings/index.d.ts b/typings/index.d.ts index dccd33049..1b1d17fc7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -434,7 +434,7 @@ declare module 'discord.js' { public presences: PresenceStore; public region: string; public roles: RoleStore; - public shard: WebSocketShard; + public readonly shard: WebSocketShard; public shardID: number; public splash: string; public readonly systemChannel: TextChannel; @@ -1295,22 +1295,22 @@ declare module 'discord.js' { public gateway: string | undefined; public readonly ping: number; public shards: Collection; - public sessionStartLimit: { total: number; remaining: number; reset_after: number; }; public status: Status; - public broadcast(packet: any): void; + + public broadcast(packet: object): void; } export class WebSocketShard extends EventEmitter { - constructor(manager: WebSocketManager, id: number, oldShard?: WebSocketShard); + constructor(manager: WebSocketManager, id: number); public id: number; public readonly ping: number; public pings: number[]; public status: Status; public manager: WebSocketManager; - public send(data: object): void; + + public send(packet: object): void; public on(event: 'ready', listener: () => void): this; - public once(event: 'ready', listener: () => void): this; } From aab3523fb5b60d21d20cbf2be01c32bdf21aa8ae Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 10 Feb 2019 20:18:08 +0000 Subject: [PATCH 0965/1359] voice: more debug information #3074, #2979, #3044 --- src/client/voice/ClientVoiceManager.js | 2 ++ src/client/voice/VoiceConnection.js | 3 +++ src/client/voice/networking/VoiceUDPClient.js | 13 +++++++++++-- src/client/voice/networking/VoiceWebSocket.js | 8 ++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index a9081efd5..fce2d3b6e 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -72,6 +72,8 @@ class ClientVoiceManager { reject(reason); }); + connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); + connection.once('authenticated', () => { connection.once('ready', () => { resolve(connection); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 0242d541d..4d5c23a86 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -396,6 +396,8 @@ class VoiceConnection extends EventEmitter { const { ws, udp } = this.sockets; + ws.on('debug', msg => this.emit('debug', msg)); + udp.on('debug', msg => this.emit('debug', msg)); ws.on('error', err => this.emit('error', err)); udp.on('error', err => this.emit('error', err)); ws.on('ready', this.onReady.bind(this)); @@ -431,6 +433,7 @@ class VoiceConnection extends EventEmitter { const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); dispatcher.on('finish', () => { clearTimeout(this.connectTimeout); + this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, * the connection will already be ready. diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index c1dd9e3d0..6c5182d97 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -48,6 +48,7 @@ class VoiceConnectionUDPClient extends EventEmitter { } shutdown() { + this.emit('debug', `[UDP] shutdown requested`); if (this.socket) { this.socket.removeAllListeners('message'); try { @@ -77,7 +78,12 @@ class VoiceConnectionUDPClient extends EventEmitter { if (!this.socket) throw new Error('UDP_SEND_FAIL'); if (!this.discordAddress || !this.discordPort) throw new Error('UDP_ADDRESS_MALFORMED'); this.socket.send(packet, 0, packet.length, this.discordPort, this.discordAddress, error => { - if (error) reject(error); else resolve(packet); + if (error) { + this.emit('debug', `[UDP] >> ERROR: ${error}`); + reject(error); + } else { + resolve(packet); + } }); }); } @@ -85,13 +91,14 @@ class VoiceConnectionUDPClient extends EventEmitter { createUDPSocket(address) { this.discordAddress = address; const socket = this.socket = udp.createSocket('udp4'); - + this.emit('debug', `[UDP] created socket`); socket.once('message', message => { // Stop if the sockets have been deleted because the connection has been closed already if (!this.voiceConnection.sockets.ws) return; const packet = parseLocalPacket(message); if (packet.error) { + this.emit('debug', `[UDP] ERROR: ${packet.error}`); this.emit('error', packet.error); return; } @@ -111,6 +118,8 @@ class VoiceConnectionUDPClient extends EventEmitter { }, }); + this.emit('debug', `[UDP] << ${JSON.stringify(packet)}`); + socket.on('message', buffer => this.voiceConnection.receiver.packets.push(buffer)); }); diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index d556145b4..ea2db4395 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -39,6 +39,7 @@ class VoiceWebSocket extends EventEmitter { } shutdown() { + this.emit('debug', `[WS] shutdown requested`); this.dead = true; this.reset(); } @@ -47,6 +48,7 @@ class VoiceWebSocket extends EventEmitter { * Resets the current WebSocket. */ reset() { + this.emit('debug', `[WS] reset requested`); if (this.ws) { if (this.ws.readyState !== WebSocket.CLOSED) this.ws.close(); this.ws = null; @@ -58,6 +60,7 @@ class VoiceWebSocket extends EventEmitter { * Starts connecting to the Voice WebSocket Server. */ connect() { + this.emit('debug', `[WS] connect requested`); if (this.dead) return; if (this.ws) this.reset(); if (this.attempts >= 5) { @@ -66,6 +69,7 @@ class VoiceWebSocket extends EventEmitter { } this.attempts++; + this.emit('debug', `[WS] connecting with ${this.attempts} attempts`); /** * The actual WebSocket used to connect to the Voice WebSocket Server. @@ -84,6 +88,7 @@ class VoiceWebSocket extends EventEmitter { * @returns {Promise} */ send(data) { + this.emit('debug', `[WS] >> ${data}`); return new Promise((resolve, reject) => { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data); this.ws.send(data, null, error => { @@ -110,6 +115,7 @@ class VoiceWebSocket extends EventEmitter { * Called whenever the WebSocket opens. */ onOpen() { + this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`); this.sendPacket({ op: OPCodes.DISPATCH, d: { @@ -140,6 +146,7 @@ class VoiceWebSocket extends EventEmitter { * Called whenever the connection to the WebSocket server is lost. */ onClose() { + this.emit('debug', `[WS] closed`); if (!this.dead) this.client.setTimeout(this.connect.bind(this), this.attempts * 1000); } @@ -156,6 +163,7 @@ class VoiceWebSocket extends EventEmitter { * @param {Object} packet The received packet */ onPacket(packet) { + this.emit('debug', `[WS] << ${JSON.stringify(packet)}`); switch (packet.op) { case VoiceOPCodes.HELLO: this.setHeartbeat(packet.d.heartbeat_interval); From 0c5b9c687058d035277450f5404e7e2e465d1282 Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 11 Feb 2019 08:41:37 +0100 Subject: [PATCH 0966/1359] chore(deps): Updated all required, peer, and development dependencies (#3073) * chore(deps): Updated all required, peer, and development dependencies And engines.node to require 10 * chore(peerDeps): Updated uws to 11.149.1 Clarified my doubts with iCrawl --- package.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index c3b4b40c4..c1db16985 100644 --- a/package.json +++ b/package.json @@ -35,36 +35,36 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { - "form-data": "^2.3.2", - "node-fetch": "^2.1.2", - "pako": "^1.0.0", + "form-data": "^2.3.3", + "node-fetch": "^2.3.0", + "pako": "^1.0.8", "prism-media": "amishshah/prism-media", "setimmediate": "^1.0.5", - "tweetnacl": "^1.0.0", - "ws": "^6.0.0" + "tweetnacl": "^1.0.1", + "ws": "^6.1.3" }, "peerDependencies": { - "@discordjs/uws": "^10.149.0", - "bufferutil": "^4.0.0", + "@discordjs/uws": "^11.149.1", + "bufferutil": "^4.0.1", "erlpack": "discordapp/erlpack", - "libsodium-wrappers": "^0.7.3", - "sodium": "^2.0.3", + "libsodium-wrappers": "^0.7.4", + "sodium": "^3.0.2", "zlib-sync": "^0.1.4" }, "devDependencies": { - "@types/node": "^10.7.1", + "@types/node": "^10.12.24", "discord.js-docgen": "discordjs/docgen", - "eslint": "^5.4.0", + "eslint": "^5.13.0", "json-filter-loader": "^1.0.0", - "terser-webpack-plugin": "^1.1.0", - "tslint": "^5.11.0", + "terser-webpack-plugin": "^1.2.2", + "tslint": "^5.12.1", "tslint-config-typings": "^0.3.1", - "typescript": "^3.0.1", - "webpack": "^4.17.0", - "webpack-cli": "^3.1.0" + "typescript": "^3.3.3", + "webpack": "^4.29.3", + "webpack-cli": "^3.2.3" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" }, "browser": { "https": false, From ab55acffdbbb50c6a28c456984165cd1fb99f4c2 Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 11 Feb 2019 17:29:20 +0100 Subject: [PATCH 0967/1359] webpack: Fix module warning when building (#3076) Warning: ``` WARNING in ./src/client/Client.js Module not found: Error: Can't resolve 'worker_threads' in 'D:\Repositories\discordjs\discord.js\src\client' @ ./src/client/Client.js @ ./src/index.js ``` --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c1db16985..41385f9dc 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "node-opus": false, "tweetnacl": false, "sodium": false, + "worker_threads": false, "zlib-sync": false, "src/sharding/Shard.js": false, "src/sharding/ShardClientUtil.js": false, From a705edfd0df4de0a7e6df59cf709132d7e0e8da2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 17:22:17 +0000 Subject: [PATCH 0968/1359] voice: more debug information --- src/client/actions/VoiceStateUpdate.js | 1 + src/client/voice/ClientVoiceManager.js | 4 ++-- src/client/voice/VoiceConnection.js | 5 +++-- src/client/websocket/handlers/VOICE_SERVER_UPDATE.js | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index e4efa1292..7e4d35149 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -26,6 +26,7 @@ class VoiceStateUpdate extends Action { // Emit event if (member && member.user.id === client.user.id && data.channel_id) { + client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); client.emit('self.voiceStateUpdate', data); } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index fce2d3b6e..ef3bb132d 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -64,6 +64,8 @@ class ClientVoiceManager { return; } else { connection = new VoiceConnection(this, channel); + connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); + connection.authenticate(); this.connections.set(channel.guild.id, connection); } @@ -72,8 +74,6 @@ class ClientVoiceManager { reject(reason); }); - connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); - connection.once('authenticated', () => { connection.once('ready', () => { resolve(connection); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 4d5c23a86..da3f8f162 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -125,8 +125,6 @@ class VoiceConnection extends EventEmitter { * @type {VoiceReceiver} */ this.receiver = new VoiceReceiver(this); - - this.authenticate(); } /** @@ -180,6 +178,9 @@ class VoiceConnection extends EventEmitter { self_deaf: false, }, options); + const queueLength = this.channel.guild.shard.ratelimit.queue.length; + this.emit('debug', `Sending voice state update (queue length is ${queueLength}): ${JSON.stringify(options)}`); + this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js index 2663d99aa..563db1ead 100644 --- a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -1,5 +1,6 @@ 'use strict'; module.exports = (client, packet) => { + client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`); client.emit('self.voiceServer', packet.d); }; From 283fc54ce49b5a0869bf7dece5e788d738b465d1 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 17:39:19 +0000 Subject: [PATCH 0969/1359] More debug information --- src/client/voice/ClientVoiceManager.js | 3 ++- src/client/voice/VoiceConnection.js | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index ef3bb132d..52b14ed19 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -64,7 +64,8 @@ class ClientVoiceManager { return; } else { connection = new VoiceConnection(this, channel); - connection.on('debug', msg => this.client.emit('debug', `[VOICE (${channel.guild.id})]: ${msg}`)); + connection.on('debug', msg => + this.client.emit('debug', `[VOICE (${channel.guild.id}:${connection.status})]: ${msg}`)); connection.authenticate(); this.connections.set(channel.guild.id, connection); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index da3f8f162..6e07c92d2 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -199,6 +199,7 @@ class VoiceConnection extends EventEmitter { // Signifies awaiting endpoint stage return; } + this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); if (!token) { this.authenticateFailed('VOICE_TOKEN_ABSENT'); @@ -206,6 +207,7 @@ class VoiceConnection extends EventEmitter { } endpoint = endpoint.match(/([^:]*)/)[0]; + this.emit('debug', `Endpoint resolved as ${endpoint}`); if (!endpoint) { this.authenticateFailed('VOICE_INVALID_ENDPOINT'); @@ -252,7 +254,7 @@ class VoiceConnection extends EventEmitter { */ checkAuthenticated() { const { token, endpoint, sessionID } = this.authentication; - + this.emit('debug', `Authenticated with sessionID ${sessionID}`); if (token && endpoint && sessionID) { this.status = VoiceStatus.CONNECTING; /** @@ -271,6 +273,7 @@ class VoiceConnection extends EventEmitter { */ authenticateFailed(reason) { clearTimeout(this.connectTimeout); + this.emit('debug', `Authenticate failed - ${reason}`); if (this.status === VoiceStatus.AUTHENTICATING) { /** * Emitted when we fail to initiate a voice connection. @@ -320,6 +323,7 @@ class VoiceConnection extends EventEmitter { this.authentication.endpoint = endpoint; this.speaking = new Speaking().freeze(); this.status = VoiceStatus.RECONNECTING; + this.emit('debug', `Reconnecting to ${endpoint}`); /** * Emitted when the voice connection is reconnecting (typically after a region change). * @event VoiceConnection#reconnecting @@ -333,6 +337,7 @@ class VoiceConnection extends EventEmitter { */ disconnect() { this.emit('closing'); + this.emit('debug', 'disconnect() triggered'); clearTimeout(this.connectTimeout); const conn = this.voiceManager.connections.get(this.channel.guild.id); if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id); @@ -366,6 +371,8 @@ class VoiceConnection extends EventEmitter { this.speaking = new Speaking().freeze(); const { ws, udp } = this.sockets; + this.emit('debug', 'Connection clean up'); + if (ws) { ws.removeAllListeners('error'); ws.removeAllListeners('ready'); @@ -384,6 +391,7 @@ class VoiceConnection extends EventEmitter { * @private */ connect() { + this.emit('debug', `Connect triggered`); if (this.status !== VoiceStatus.RECONNECTING) { if (this.sockets.ws) throw new Error('WS_CONNECTION_EXISTS'); if (this.sockets.udp) throw new Error('UDP_CONNECTION_EXISTS'); From 810b5a5f7175cb916ed3b9fea23ebf623eed8147 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 17:57:56 +0000 Subject: [PATCH 0970/1359] voice: more debug info --- src/client/voice/ClientVoiceManager.js | 2 ++ src/client/voice/VoiceConnection.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 52b14ed19..e65effdd7 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -28,12 +28,14 @@ class ClientVoiceManager { } onVoiceServer({ guild_id, token, endpoint }) { + this.client.emit('debug', `[VOICE] voiceServer guild: ${guild_id} token: ${token} endpoint: ${endpoint}`); const connection = this.connections.get(guild_id); if (connection) connection.setTokenAndEndpoint(token, endpoint); } onVoiceStateUpdate({ guild_id, session_id, channel_id }) { const connection = this.connections.get(guild_id); + this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`); if (!connection) return; if (!channel_id && connection.status !== VoiceStatus.DISCONNECTED) { connection._disconnect(); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6e07c92d2..6a945731c 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -195,11 +195,11 @@ class VoiceConnection extends EventEmitter { * @returns {void} */ setTokenAndEndpoint(token, endpoint) { + this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); if (!endpoint) { // Signifies awaiting endpoint stage return; } - this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`); if (!token) { this.authenticateFailed('VOICE_TOKEN_ABSENT'); @@ -229,6 +229,7 @@ class VoiceConnection extends EventEmitter { * @private */ setSessionID(sessionID) { + this.emit('debug', `Setting sessionID ${sessionID} (stored as "${this.authentication.sessionID}")`); if (!sessionID) { this.authenticateFailed('VOICE_SESSION_ABSENT'); return; From 375706beac3219ae20885681112289bb2e0c6604 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 18:03:35 +0000 Subject: [PATCH 0971/1359] voice: replace self.xyz events --- src/client/actions/VoiceStateUpdate.js | 2 +- src/client/voice/ClientVoiceManager.js | 3 --- src/client/websocket/handlers/VOICE_SERVER_UPDATE.js | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 7e4d35149..045ca9319 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -27,7 +27,7 @@ class VoiceStateUpdate extends Action { // Emit event if (member && member.user.id === client.user.id && data.channel_id) { client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); - client.emit('self.voiceStateUpdate', data); + client.voice.onVoiceStateUpdate(data); } /** diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index e65effdd7..466b6d664 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -22,9 +22,6 @@ class ClientVoiceManager { * @type {Collection} */ this.connections = new Collection(); - - this.client.on('self.voiceServer', this.onVoiceServer.bind(this)); - this.client.on('self.voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); } onVoiceServer({ guild_id, token, endpoint }) { diff --git a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js index 563db1ead..f9cf53433 100644 --- a/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js +++ b/src/client/websocket/handlers/VOICE_SERVER_UPDATE.js @@ -2,5 +2,5 @@ module.exports = (client, packet) => { client.emit('debug', `[VOICE] received voice server: ${JSON.stringify(packet)}`); - client.emit('self.voiceServer', packet.d); + client.voice.onVoiceServer(packet.d); }; From fe51b4e89b2f4ea256fb20ad44810e7f64ac8b53 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 18:28:37 +0000 Subject: [PATCH 0972/1359] voice: more debug information, correctly listen to vws --- src/client/voice/VoiceConnection.js | 2 ++ src/client/voice/networking/VoiceWebSocket.js | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 6a945731c..d0922ae81 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -413,6 +413,8 @@ class VoiceConnection extends EventEmitter { ws.on('ready', this.onReady.bind(this)); ws.on('sessionDescription', this.onSessionDescription.bind(this)); ws.on('speaking', this.onSpeaking.bind(this)); + + this.sockets.ws.connect(); } /** diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index ea2db4395..e238c62bd 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -25,7 +25,6 @@ class VoiceWebSocket extends EventEmitter { */ this.attempts = 0; - this.connect(); this.dead = false; this.connection.on('closing', this.shutdown.bind(this)); } @@ -69,7 +68,7 @@ class VoiceWebSocket extends EventEmitter { } this.attempts++; - this.emit('debug', `[WS] connecting with ${this.attempts} attempts`); + this.emit('debug', `[WS] connecting, ${this.attempts} attempts, wss://${this.connection.authentication.endpoint}/`); /** * The actual WebSocket used to connect to the Voice WebSocket Server. From c362ba0dd549ac0a2be850b81966ffa29b030db5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 11 Feb 2019 18:37:33 +0000 Subject: [PATCH 0973/1359] voice: more debug --- src/client/voice/networking/VoiceWebSocket.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index e238c62bd..3720afa74 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -68,13 +68,13 @@ class VoiceWebSocket extends EventEmitter { } this.attempts++; - this.emit('debug', `[WS] connecting, ${this.attempts} attempts, wss://${this.connection.authentication.endpoint}/`); /** * The actual WebSocket used to connect to the Voice WebSocket Server. * @type {WebSocket} */ this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 4 }); + this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`); this.ws.onopen = this.onOpen.bind(this); this.ws.onmessage = this.onMessage.bind(this); this.ws.onclose = this.onClose.bind(this); @@ -154,6 +154,7 @@ class VoiceWebSocket extends EventEmitter { * @param {Error} error The error that occurred */ onError(error) { + this.emit('debug', `[WS] Error: ${error}`); this.emit('error', error); } From 0c7a618f1017bf6cf807562799c606ec72fce140 Mon Sep 17 00:00:00 2001 From: Kyra Date: Mon, 11 Feb 2019 20:33:13 +0100 Subject: [PATCH 0974/1359] cleanup: remove unused members of classes (#3078) * cleanup: Remove unused members of classes * typings: Remove hit from Message --- src/errors/Messages.js | 2 -- src/structures/DMChannel.js | 1 - src/structures/GroupDMChannel.js | 1 - src/structures/Message.js | 6 ------ src/structures/TextChannel.js | 1 - typings/index.d.ts | 1 - 6 files changed, 12 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 792c2b8b9..443315a7b 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -81,8 +81,6 @@ const Messages = { PRUNE_DAYS_TYPE: 'Days must be a number', - SEARCH_CHANNEL_TYPE: 'Target must be a TextChannel, DMChannel, GroupDMChannel, or Guild.', - GUILD_CHANNEL_RESOLVE: 'Could not resolve channel to a guild channel.', GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 5549618a2..7c3db02e6 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -59,7 +59,6 @@ class DMChannel extends Channel { get lastMessage() {} get lastPinAt() {} send() {} - search() {} startTyping() {} stopTyping() {} get typing() {} diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 6f9aca390..39fb56468 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -229,7 +229,6 @@ class GroupDMChannel extends Channel { get lastMessage() {} get lastPinAt() {} send() {} - search() {} startTyping() {} stopTyping() {} get typing() {} diff --git a/src/structures/Message.js b/src/structures/Message.js index 1947705ec..310706e15 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -153,12 +153,6 @@ class Message extends Base { type: data.activity.type, } : null; - /** - * Whether this message is a hit in a search - * @type {?boolean} - */ - this.hit = typeof data.hit === 'boolean' ? data.hit : null; - /** * The previous versions of the message, sorted with the most recent first * @type {Message[]} diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 47c8e19c2..a643815e5 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -127,7 +127,6 @@ class TextChannel extends GuildChannel { get lastMessage() {} get lastPinAt() {} send() {} - search() {} startTyping() {} stopTyping() {} get typing() {} diff --git a/typings/index.d.ts b/typings/index.d.ts index 1b1d17fc7..8c606400c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -661,7 +661,6 @@ declare module 'discord.js' { public readonly edits: Message[]; public embeds: MessageEmbed[]; public readonly guild: Guild; - public hit: boolean; public id: Snowflake; public readonly member: GuildMember; public mentions: MessageMentions; From 5c19ad72525188b44d1c26ba59424a43e763c266 Mon Sep 17 00:00:00 2001 From: Kyra Date: Tue, 12 Feb 2019 09:13:15 +0100 Subject: [PATCH 0975/1359] cleanup: Error messages cleanup (#3079) Removed keys that return zero occurrences in the library's source code --- src/errors/Messages.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 443315a7b..5945c167f 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -15,12 +15,8 @@ const Messages = { BITFIELD_INVALID: 'Invalid bitfield flag or number.', - RATELIMIT_INVALID_METHOD: 'Unknown rate limiting method.', - SHARDING_INVALID: 'Invalid shard settings were provided.', SHARDING_REQUIRED: 'This session would have handled too many guilds - Sharding is required.', - SHARDING_CHILD_CONNECTION: 'Failed to send message to shard\'s process.', - SHARDING_PARENT_CONNECTION: 'Failed to send message to master process.', SHARDING_NO_SHARDS: 'No shards have been spawned.', SHARDING_IN_PROCESS: 'Shards are still being spawned.', SHARDING_ALREADY_SPAWNED: count => `Already spawned ${count} shards.`, @@ -41,7 +37,6 @@ const Messages = { VOICE_INVALID_HEARTBEAT: 'Tried to set voice heartbeat but no valid interval was specified.', VOICE_USER_MISSING: 'Couldn\'t resolve the user to create stream.', - VOICE_STREAM_EXISTS: 'There is already an existing stream for that user.', VOICE_JOIN_CHANNEL: (full = false) => `You do not have permission to join this voice channel${full ? '; it is full.' : '.'}`, VOICE_CONNECTION_TIMEOUT: 'Connection not established within 15 seconds.', @@ -57,19 +52,15 @@ const Messages = { VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', - OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', - UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.', UDP_CONNECTION_EXISTS: 'There is already an existing UDP connection.', - REQ_BODY_TYPE: 'The response body isn\'t a Buffer.', REQ_RESOURCE_TYPE: 'The resource must be a string, Buffer or a valid file stream.', IMAGE_FORMAT: format => `Invalid image format: ${format}`, IMAGE_SIZE: size => `Invalid image size: ${size}`, - MESSAGE_MISSING: 'Message not found', MESSAGE_BULK_DELETE_TYPE: 'The messages must be an Array, Collection, or number.', MESSAGE_NONCE_TYPE: 'Message nonce must fit in an unsigned 64-bit integer.', @@ -85,7 +76,6 @@ const Messages = { GUILD_VOICE_CHANNEL_RESOLVE: 'Could not resolve channel to a guild voice channel.', GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', GUILD_OWNED: 'Guild is owned by the client.', - GUILD_RESTRICTED: (state = false) => `Guild is ${state ? 'already' : 'not'} restricted.`, GUILD_MEMBERS_TIMEOUT: 'Members didn\'t arrive in time.', INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, From 8910fed7298d5fb3d9b97d979bd1f0e0bd901145 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 12 Feb 2019 16:29:11 +0000 Subject: [PATCH 0976/1359] voice: debug UDP (#3044) --- src/client/voice/networking/VoiceUDPClient.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/client/voice/networking/VoiceUDPClient.js b/src/client/voice/networking/VoiceUDPClient.js index 6c5182d97..3c4935064 100644 --- a/src/client/voice/networking/VoiceUDPClient.js +++ b/src/client/voice/networking/VoiceUDPClient.js @@ -88,11 +88,19 @@ class VoiceConnectionUDPClient extends EventEmitter { }); } - createUDPSocket(address) { + async createUDPSocket(address) { this.discordAddress = address; const socket = this.socket = udp.createSocket('udp4'); + socket.on('error', e => { + this.emit('debug', `[UDP] Error: ${e}`); + this.emit('error', e); + }); + socket.on('close', () => { + this.emit('debug', '[UDP] socket closed'); + }); this.emit('debug', `[UDP] created socket`); socket.once('message', message => { + this.emit('debug', `[UDP] message: [${[...message]}] (${message})`); // Stop if the sockets have been deleted because the connection has been closed already if (!this.voiceConnection.sockets.ws) return; @@ -125,7 +133,9 @@ class VoiceConnectionUDPClient extends EventEmitter { const blankMessage = Buffer.alloc(70); blankMessage.writeUIntBE(this.voiceConnection.authentication.ssrc, 0, 4); - this.send(blankMessage); + this.emit('debug', `Sending IP discovery packet: [${[...blankMessage]}]`); + await this.send(blankMessage); + this.emit('debug', `Successfully sent IP discovery packet`); } } From 5c3f5d704840fca3888264e19eb38d88977304ea Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 13 Feb 2019 17:39:39 +0000 Subject: [PATCH 0977/1359] Partials (#3070) * Remove GroupDMChannels they sparked no joy * Start partials for message deletion * MessageUpdate partials * Add partials as an opt-in client option * Add fetch() to Message * Message.author should never be undefined * Fix channels being the wrong type * Allow fetching channels * Refactor and add reaction add partials * Reaction remove partials * Check for emoji first * fix message fetching janky * User partials in audit logs * refactor overwrite code * guild member partials * partials as a whitelist * document GuildMember#fetch * fix: check whether a structure is a partial, not whether cache is true * typings: Updated for latest commit (#3075) * partials: fix messageUpdate behaviour (now "old" message can be partial) * partials: add warnings and docs * partials: add partials to index.yml * partials: tighten "partial" definitions * partials: fix embed-only messages counting as partials --- docs/index.yml | 2 + docs/topics/partials.md | 61 +++++ src/client/Client.js | 3 + src/client/actions/Action.js | 23 ++ src/client/actions/ChannelCreate.js | 2 +- src/client/actions/ChannelDelete.js | 2 +- src/client/actions/MessageDelete.js | 5 +- src/client/actions/MessageReactionAdd.js | 10 +- src/client/actions/MessageReactionRemove.js | 10 +- .../actions/MessageReactionRemoveAll.js | 6 +- src/client/actions/MessageUpdate.js | 7 +- .../websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- .../websocket/handlers/CHANNEL_UPDATE.js | 4 +- src/index.js | 1 - src/stores/ChannelStore.js | 10 +- src/stores/DataStore.js | 1 + src/stores/GuildMemberStore.js | 2 +- src/stores/MessageStore.js | 15 +- src/stores/UserStore.js | 8 +- src/structures/APIMessage.js | 2 +- src/structures/Channel.js | 14 +- src/structures/ClientUser.js | 36 --- src/structures/DMChannel.js | 22 +- src/structures/GroupDMChannel.js | 245 ------------------ src/structures/Guild.js | 6 +- src/structures/GuildAuditLogs.js | 15 +- src/structures/GuildMember.js | 18 +- src/structures/Message.js | 32 ++- src/structures/MessageCollector.js | 2 +- src/structures/User.js | 18 ++ src/util/Constants.js | 21 ++ src/util/Structures.js | 1 - src/util/Util.js | 4 +- test/voice.js | 11 +- typings/index.d.ts | 57 ++-- 35 files changed, 295 insertions(+), 383 deletions(-) create mode 100644 docs/topics/partials.md delete mode 100644 src/structures/GroupDMChannel.js diff --git a/docs/index.yml b/docs/index.yml index 7c5b3b280..175780574 100644 --- a/docs/index.yml +++ b/docs/index.yml @@ -12,6 +12,8 @@ path: voice.md - name: Web builds path: web.md + - name: Partials + path: partials.md - name: Examples files: - name: Ping diff --git a/docs/topics/partials.md b/docs/topics/partials.md new file mode 100644 index 000000000..2566f3def --- /dev/null +++ b/docs/topics/partials.md @@ -0,0 +1,61 @@ +# Partials + +Partials allow you to receive events that contain uncached instances, providing structures that contain very minimal +data. For example, if you were to receive a `messageDelete` event with an uncached message, normally Discord.js would +discard the event. With partials, you're able to receive the event, with a Message object that contains just an ID. + +## Opting in + +Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): + +```js +// Accept partial messages and DM channels when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL'] }); +``` + +## Usage & warnings + +The only guaranteed data a partial structure can store is its ID. All other properties/methods should be +considered invalid/defunct while accessing a partial structure. + +After opting-in with the above, you begin to allow partial messages and channels in your caches, so it's important +to check whether they're safe to access whenever you encounter them, whether it be in events or through normal cache +usage. + +All instance of structures that you opted-in for will have a `partial` property. As you'd expect, this value is `true` +when the instance is partial. Partial structures are only guaranteed to contain an ID, any other properties and methods +no longer carry their normal type guarantees. + +This means you have to take time to consider possible parts of your program that might need checks put in place to +prevent accessing partial data: + +```js +client.on('messageDelete', message => { + console.log(`${message.id} was deleted!`); + // Partial messages do not contain any content so skip them + if (!message.partial) { + console.log(`It had content: "${message.content}"`); + } +}) + +// You can also try to upgrade partials to full instances: +client.on('messageReactionAdd', async (reaction, user) => { + // If a message gains a reaction and it is uncached, fetch and cache the message + // You should account for any errors while fetching, it could return API errors if the resource is missing + if (reaction.message.partial) await reaction.message.fetch(); + // Now the message has been cached and is fully available: + console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); +}); +``` + +If a message is deleted and both the message and channel are uncached, you must enable both 'MESSAGE' and +'CHANNEL' in the client options to receive the messageDelete event. + +## Why? + +This allows developers to listen to events that contain uncached data, which is useful if you're running a moderation +bot or any bot that relies on still receiving updates to resources you don't have cached -- message reactions are a +good example. + +Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should +not be cached. \ No newline at end of file diff --git a/src/client/Client.js b/src/client/Client.js index 4e5f5e247..b1dc15573 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -434,6 +434,9 @@ class Client extends BaseClient { if (typeof options.disableEveryone !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); } + if (!(options.partials instanceof Array)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); + } if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); } diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 791eaa00c..09faae9e2 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -1,5 +1,7 @@ 'use strict'; +const { PartialTypes } = require('../../util/Constants'); + /* ABOUT ACTIONS @@ -20,6 +22,27 @@ class GenericAction { handle(data) { return data; } + + getChannel(data) { + const id = data.channel_id || data.id; + return data.channel || (this.client.options.partials.includes(PartialTypes.CHANNEL) ? + this.client.channels.add({ + id, + guild_id: data.guild_id, + }) : + this.client.channels.get(id)); + } + + getMessage(data, channel) { + const id = data.message_id || data.id; + return data.message || (this.client.options.partials.includes(PartialTypes.MESSAGE) ? + channel.messages.add({ + id, + channel_id: channel.id, + guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), + }) : + channel.messages.get(id)); + } } module.exports = GenericAction; diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 4a9d17d45..6830f2ab5 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -12,7 +12,7 @@ class ChannelCreateAction extends Action { /** * Emitted whenever a channel is created. * @event Client#channelCreate - * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was created + * @param {DMChannel|GuildChannel} channel The channel that was created */ client.emit(Events.CHANNEL_CREATE, channel); } diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index b9909f8ba..9fc0e6d99 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -19,7 +19,7 @@ class ChannelDeleteAction extends Action { /** * Emitted whenever a channel is deleted. * @event Client#channelDelete - * @param {DMChannel|GroupDMChannel|GuildChannel} channel The channel that was deleted + * @param {DMChannel|GuildChannel} channel The channel that was deleted */ client.emit(Events.CHANNEL_DELETE, channel); } diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index d66d10a28..feb118c10 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -6,11 +6,10 @@ const { Events } = require('../../util/Constants'); class MessageDeleteAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = this.getChannel(data); let message; - if (channel) { - message = channel.messages.get(data.id); + message = this.getMessage(data, channel); if (message) { channel.messages.delete(message.id); message.deleted = true; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index a800c3c7a..b7400cf77 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -11,15 +11,19 @@ const Action = require('./Action'); class MessageReactionAdd extends Action { handle(data) { + if (!data.emoji) return false; + const user = data.user || this.client.users.get(data.user_id); if (!user) return false; + // Verify channel - const channel = data.channel || this.client.channels.get(data.channel_id); + const channel = this.getChannel(data); if (!channel || channel.type === 'voice') return false; + // Verify message - const message = data.message || channel.messages.get(data.message_id); + const message = this.getMessage(data, channel); if (!message) return false; - if (!data.emoji) return false; + // Verify reaction const reaction = message.reactions.add({ emoji: data.emoji, diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 7ab5be289..e5d4f8413 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -12,15 +12,19 @@ const { Events } = require('../../util/Constants'); class MessageReactionRemove extends Action { handle(data) { + if (!data.emoji) return false; + const user = this.client.users.get(data.user_id); if (!user) return false; + // Verify channel - const channel = this.client.channels.get(data.channel_id); + const channel = this.getChannel(data); if (!channel || channel.type === 'voice') return false; + // Verify message - const message = channel.messages.get(data.message_id); + const message = this.getMessage(data, channel); if (!message) return false; - if (!data.emoji) return false; + // Verify reaction const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); const reaction = message.reactions.get(emojiID); diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index f32730977..0921ce50e 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -5,10 +5,12 @@ const { Events } = require('../../util/Constants'); class MessageReactionRemoveAll extends Action { handle(data) { - const channel = this.client.channels.get(data.channel_id); + // Verify channel + const channel = this.getChannel(data); if (!channel || channel.type === 'voice') return false; - const message = channel.messages.get(data.message_id); + // Verify message + const message = this.getMessage(data, channel); if (!message) return false; message.reactions.clear(); diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js index be26b2692..07e2aacb3 100644 --- a/src/client/actions/MessageUpdate.js +++ b/src/client/actions/MessageUpdate.js @@ -4,11 +4,10 @@ const Action = require('./Action'); class MessageUpdateAction extends Action { handle(data) { - const client = this.client; - - const channel = client.channels.get(data.channel_id); + const channel = this.getChannel(data); if (channel) { - const message = channel.messages.get(data.id); + const { id, channel_id, guild_id, author, timestamp, type } = data; + const message = this.getMessage({ id, channel_id, guild_id, author, timestamp, type }, channel); if (message) { message.patch(data); return { diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index dfc854e37..11154674a 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -14,7 +14,7 @@ module.exports = (client, { d: data }) => { * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, * not much information can be provided easily here - you need to manually check the pins yourself. * @event Client#channelPinsUpdate - * @param {DMChannel|GroupDMChannel|TextChannel} channel The channel that the pins update occured in + * @param {DMChannel|TextChannel} channel The channel that the pins update occured in * @param {Date} time The time of the pins update */ client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js index b437ebbde..7a0df486a 100644 --- a/src/client/websocket/handlers/CHANNEL_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -8,8 +8,8 @@ module.exports = (client, packet) => { /** * Emitted whenever a channel is updated - e.g. name change, topic change. * @event Client#channelUpdate - * @param {DMChannel|GroupDMChannel|GuildChannel} oldChannel The channel before the update - * @param {DMChannel|GroupDMChannel|GuildChannel} newChannel The channel after the update + * @param {DMChannel|GuildChannel} oldChannel The channel before the update + * @param {DMChannel|GuildChannel} newChannel The channel after the update */ client.emit(Events.CHANNEL_UPDATE, old, updated); } diff --git a/src/index.js b/src/index.js index d9b8cb451..b3a92f60c 100644 --- a/src/index.js +++ b/src/index.js @@ -65,7 +65,6 @@ module.exports = { Collector: require('./structures/interfaces/Collector'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), - GroupDMChannel: require('./structures/GroupDMChannel'), Guild: require('./structures/Guild'), GuildAuditLogs: require('./structures/GuildAuditLogs'), GuildChannel: require('./structures/GuildChannel'), diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 9ce6465cf..88c5a0bb3 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -5,7 +5,7 @@ const Channel = require('../structures/Channel'); const { Events } = require('../util/Constants'); const kLru = Symbol('LRU'); -const lruable = ['group', 'dm']; +const lruable = ['dm']; /** * Stores channels. @@ -54,6 +54,7 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); + if (existing && existing.partial && cache) existing._patch(data); if (existing) return existing; const channel = Channel.create(this.client, data, guild); @@ -85,11 +86,12 @@ class ChannelStore extends DataStore { * .then(channel => console.log(channel.name)) * .catch(console.error); */ - fetch(id, cache = true) { + async fetch(id, cache = true) { const existing = this.get(id); - if (existing) return Promise.resolve(existing); + if (existing && !existing.partial) return existing; - return this.client.api.channels(id).get().then(data => this.add(data, null, cache)); + const data = await this.client.api.channels(id).get(); + return this.add(data, null, cache); } /** diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index fc841899a..ade8c70f7 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -18,6 +18,7 @@ class DataStore extends Collection { add(data, cache = true, { id, extras = [] } = {}) { const existing = this.get(id || data.id); + if (existing && existing.partial && cache && existing._patch) existing._patch(data); if (existing) return existing; const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 039fb4c7b..397c119b7 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -180,7 +180,7 @@ class GuildMemberStore extends DataStore { _fetchSingle({ user, cache }) { const existing = this.get(user); - if (existing && existing.joinedTimestamp) return Promise.resolve(existing); + if (existing && !existing.partial) return Promise.resolve(existing); return this.client.api.guilds(this.guild.id).members(user).get() .then(data => this.add(data, cache)); } diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 1a8a7083a..52bf7fdd6 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -40,6 +40,7 @@ class MessageStore extends DataStore { * The returned Collection does not contain reaction users of the messages if they were not cached. * Those need to be fetched separately in such a case. * @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters. + * @param {boolean} [cache=true] Whether to cache the message(s) * @returns {Promise|Promise>} * @example * // Get message @@ -57,8 +58,8 @@ class MessageStore extends DataStore { * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) * .catch(console.error); */ - fetch(message) { - return typeof message === 'string' ? this._fetchId(message) : this._fetchMany(message); + fetch(message, cache = true) { + return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache); } /** @@ -80,15 +81,17 @@ class MessageStore extends DataStore { }); } - async _fetchId(messageID) { + async _fetchId(messageID, cache) { + const existing = this.get(messageID); + if (existing && !existing.partial) return existing; const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); - return this.add(data); + return this.add(data, cache); } - async _fetchMany(options = {}) { + async _fetchMany(options = {}, cache) { const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message)); + for (const message of data) messages.set(message.id, this.add(message, cache)); return messages; } diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 8fd2d9d5a..20c25d05b 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -51,11 +51,11 @@ class UserStore extends DataStore { * @param {boolean} [cache=true] Whether to cache the new user object if it isn't already * @returns {Promise} */ - fetch(id, cache = true) { + async fetch(id, cache = true) { const existing = this.get(id); - if (existing) return Promise.resolve(existing); - - return this.client.api.users(id).get().then(data => this.add(data, cache)); + if (existing && !existing.partial) return existing; + const data = await this.client.api.users(id).get(); + return this.add(data, cache); } } diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index deca5d26d..41f983839 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -330,7 +330,7 @@ module.exports = APIMessage; /** * A target for a message. - * @typedef {TextChannel|DMChannel|GroupDMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget + * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient} MessageTarget */ /** diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 92914118d..b43411949 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -16,7 +16,6 @@ class Channel extends Base { /** * The type of the channel, either: * * `dm` - a DM channel - * * `group` - a Group DM channel * * `text` - a guild text channel * * `voice` - a guild voice channel * * `category` - a guild category channel @@ -84,15 +83,20 @@ class Channel extends Base { return this.client.api.channels(this.id).delete().then(() => this); } + /** + * Fetches this channel. + * @returns {Promise} + */ + fetch() { + return this.client.channels.fetch(this.id, true); + } + static create(client, data, guild) { const Structures = require('../util/Structures'); let channel; - if (data.type === ChannelTypes.DM) { + if (data.type === ChannelTypes.DM || (data.type !== ChannelTypes.GROUP && !data.guild_id && !guild)) { const DMChannel = Structures.get('DMChannel'); channel = new DMChannel(client, data); - } else if (data.type === ChannelTypes.GROUP) { - const GroupDMChannel = Structures.get('GroupDMChannel'); - channel = new GroupDMChannel(client, data); } else { guild = guild || client.guilds.get(data.guild_id); if (guild) { diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 3ec32e9dd..a86a6cb56 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -164,42 +164,6 @@ class ClientUser extends Structures.get('User') { setAFK(afk) { return this.setPresence({ afk }); } - - /** - * An object containing either a user or access token, and an optional nickname. - * @typedef {Object} GroupDMRecipientOptions - * @property {UserResolvable} [user] User to add to the Group DM - * @property {string} [accessToken] Access token to use to add a user to the Group DM - * (only available if a bot is creating the DM) - * @property {string} [nick] Permanent nickname (only available if a bot is creating the DM) - * @property {string} [id] If no user resolvable is provided and you want to assign nicknames - * you must provide user ids instead - */ - - /** - * Creates a Group DM. - * @param {GroupDMRecipientOptions[]} recipients The recipients - * @returns {Promise} - * @example - * // Create a Group DM with a token provided from OAuth - * client.user.createGroupDM([{ - * user: '66564597481480192', - * accessToken: token - * }]) - * .then(console.log) - * .catch(console.error); - */ - createGroupDM(recipients) { - const data = this.bot ? { - access_tokens: recipients.map(u => u.accessToken), - nicks: recipients.reduce((o, r) => { - if (r.nick) o[r.user ? r.user.id : r.id] = r.nick; - return o; - }, {}), - } : { recipients: recipients.map(u => this.client.users.resolveID(u.user || u.id)) }; - return this.client.api.users('@me').channels.post({ data }) - .then(res => this.client.channels.add(res)); - } } module.exports = ClientUser; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 7c3db02e6..f1280e802 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -12,6 +12,8 @@ const MessageStore = require('../stores/MessageStore'); class DMChannel extends Channel { constructor(client, data) { super(client, data); + // Override the channel type so partials have a known type + this.type = 'dm'; /** * A collection containing the messages sent to this channel * @type {MessageStore} @@ -23,11 +25,13 @@ class DMChannel extends Channel { _patch(data) { super._patch(data); - /** - * The recipient on the other end of the DM - * @type {User} - */ - this.recipient = this.client.users.add(data.recipients[0]); + if (data.recipients) { + /** + * The recipient on the other end of the DM + * @type {User} + */ + this.recipient = this.client.users.add(data.recipients[0]); + } /** * The ID of the last message in the channel, if one was sent @@ -42,6 +46,14 @@ class DMChannel extends Channel { this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; } + /** + * Whether this DMChannel is a partial + * @type {boolean} + */ + get partial() { + return !this.recipient; + } + /** * When concatenated with a string, this automatically returns the recipient's mention instead of the * DMChannel object. diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js deleted file mode 100644 index 39fb56468..000000000 --- a/src/structures/GroupDMChannel.js +++ /dev/null @@ -1,245 +0,0 @@ -'use strict'; - -const Channel = require('./Channel'); -const TextBasedChannel = require('./interfaces/TextBasedChannel'); -const Collection = require('../util/Collection'); -const DataResolver = require('../util/DataResolver'); -const MessageStore = require('../stores/MessageStore'); - -/* -{ type: 3, - recipients: - [ { username: 'Charlie', - id: '123', - discriminator: '6631', - avatar: '123' }, - { username: 'Ben', - id: '123', - discriminator: '2055', - avatar: '123' }, - { username: 'Adam', - id: '123', - discriminator: '2406', - avatar: '123' } ], - owner_id: '123', - name: null, - last_message_id: '123', - id: '123', - icon: null } -*/ - -/** - * Represents a Group DM on Discord. - * @extends {Channel} - * @implements {TextBasedChannel} - */ -class GroupDMChannel extends Channel { - constructor(client, data) { - super(client, data); - /** - * A collection containing the messages sent to this channel - * @type {MessageStore} - */ - this.messages = new MessageStore(this); - this._typing = new Map(); - } - - _patch(data) { - super._patch(data); - - /** - * The name of this Group DM, can be null if one isn't set - * @type {string} - */ - this.name = data.name; - - /** - * A hash of this Group DM icon - * @type {?string} - */ - this.icon = data.icon; - - /** - * The user ID of this Group DM's owner - * @type {Snowflake} - */ - this.ownerID = data.owner_id; - - /** - * If the DM is managed by an application - * @type {boolean} - */ - this.managed = data.managed; - - /** - * Application ID of the application that made this Group DM, if applicable - * @type {?Snowflake} - */ - this.applicationID = data.application_id; - - if (data.nicks) { - /** - * Nicknames for group members - * @type {?Collection} - */ - this.nicks = new Collection(data.nicks.map(n => [n.id, n.nick])); - } - - if (!this.recipients) { - /** - * A collection of the recipients of this DM, mapped by their ID - * @type {Collection} - */ - this.recipients = new Collection(); - } - - if (data.recipients) { - for (const recipient of data.recipients) { - const user = this.client.users.add(recipient); - this.recipients.set(user.id, user); - } - } - - /** - * The ID of the last message in the channel, if one was sent - * @type {?Snowflake} - */ - this.lastMessageID = data.last_message_id; - - /** - * The timestamp when the last pinned message was pinned, if there was one - * @type {?number} - */ - this.lastPinTimestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp).getTime() : null; - } - - /** - * The owner of this Group DM - * @type {?User} - * @readonly - */ - get owner() { - return this.client.users.get(this.ownerID) || null; - } - - /** - * Gets the URL to this Group DM's icon. - * @param {ImageURLOptions} [options={}] Options for the Image URL - * @returns {?string} - */ - iconURL({ format, size } = {}) { - if (!this.icon) return null; - return this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); - } - - /** - * Whether this channel equals another channel. It compares all properties, so for most operations - * it is advisable to just compare `channel.id === channel2.id` as it is much faster and is often - * what most users need. - * @param {GroupDMChannel} channel Channel to compare with - * @returns {boolean} - */ - equals(channel) { - const equal = channel && - this.id === channel.id && - this.name === channel.name && - this.icon === channel.icon && - this.ownerID === channel.ownerID; - - if (equal) { - return this.recipients.equals(channel.recipients); - } - - return equal; - } - - /** - * Edits this Group DM. - * @param {Object} data New data for this Group DM - * @param {string} [reason] Reason for editing this Group DM - * @returns {Promise} - */ - edit(data, reason) { - return this.client.api.channels[this.id].patch({ - data: { - icon: data.icon, - name: data.name === null ? null : data.name || this.name, - }, - reason, - }).then(() => this); - } - - /** - * Sets a new icon for this Group DM. - * @param {Base64Resolvable|BufferResolvable} icon The new icon of this Group DM - * @returns {Promise} - */ - async setIcon(icon) { - return this.edit({ icon: await DataResolver.resolveImage(icon) }); - } - - /** - * Sets a new name for this Group DM. - * @param {string} name New name for this Group DM - * @returns {Promise} - */ - setName(name) { - return this.edit({ name }); - } - - /** - * Adds a user to this Group DM. - * @param {Object} options Options for this method - * @param {UserResolvable} options.user User to add to this Group DM - * @param {string} [options.accessToken] Access token to use to add the user to this Group DM - * @param {string} [options.nick] Permanent nickname to give the user - * @returns {Promise} - */ - addUser({ user, accessToken, nick }) { - const id = this.client.users.resolveID(user); - return this.client.api.channels[this.id].recipients[id].put({ nick, access_token: accessToken }) - .then(() => this); - } - - /** - * Removes a user from this Group DM. - * @param {UserResolvable} user User to remove - * @returns {Promise} - */ - removeUser(user) { - const id = this.client.users.resolveID(user); - return this.client.api.channels[this.id].recipients[id].delete() - .then(() => this); - } - - /** - * When concatenated with a string, this automatically returns the channel's name instead of the - * GroupDMChannel object. - * @returns {string} - * @example - * // Logs: Hello from My Group DM! - * console.log(`Hello from ${channel}!`); - */ - toString() { - return this.name; - } - - // These are here only for documentation purposes - they are implemented by TextBasedChannel - /* eslint-disable no-empty-function */ - get lastMessage() {} - get lastPinAt() {} - send() {} - startTyping() {} - stopTyping() {} - get typing() {} - get typingCount() {} - createMessageCollector() {} - awaitMessages() {} - // Doesn't work on Group DMs; bulkDelete() {} - acknowledge() {} - _cacheMessage() {} -} - -TextBasedChannel.applyToClass(GroupDMChannel, true, ['bulkDelete']); - -module.exports = GroupDMChannel; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 66cf98718..f99627a4c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,7 @@ const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); -const { ChannelTypes, DefaultMessageNotifications, browser } = require('../util/Constants'); +const { ChannelTypes, DefaultMessageNotifications, PartialTypes, browser } = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); @@ -341,7 +341,9 @@ class Guild extends Base { * @readonly */ get owner() { - return this.members.get(this.ownerID) || null; + return this.members.get(this.ownerID) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + this.members.add({ user: { id: this.ownerID } }, true) : + null); } /** diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index f78078a7c..b466a3aa0 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -4,6 +4,7 @@ const Collection = require('../util/Collection'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); const Util = require('../util/Util'); +const PartialTypes = require('../util/Constants'); /** * The target type of an entry, e.g. `GUILD`. Here are the available types: @@ -234,7 +235,7 @@ class GuildAuditLogs { * Audit logs entry. */ class GuildAuditLogsEntry { - constructor(logs, guild, data) { + constructor(logs, guild, data) { // eslint-disable-line complexity const targetType = GuildAuditLogs.targetType(data.action_type); /** * The target type of this entry @@ -264,7 +265,9 @@ class GuildAuditLogsEntry { * The user that executed this entry * @type {User} */ - this.executor = guild.client.users.get(data.user_id); + this.executor = guild.client.options.partials.includes(PartialTypes.USER) ? + guild.client.users.add({ id: data.user_id }) : + guild.client.users.get(data.user_id); /** * An entry in the audit log representing a specific change. @@ -329,8 +332,12 @@ class GuildAuditLogsEntry { return o; }, {}); this.target.id = data.target_id; - } else if ([Targets.USER, Targets.GUILD].includes(targetType)) { - this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id); + } else if (targetType === Targets.USER) { + this.target = guild.client.options.partials.includes(PartialTypes.USER) ? + guild.client.users.add({ id: data.target_id }) : + guild.client.users.get(data.target_id); + } else if (targetType === Targets.GUILD) { + this.target = guild.client.guilds.get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = logs.webhooks.get(data.target_id) || new Webhook(guild.client, diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 73adb69ac..e2dea4322 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -28,7 +28,7 @@ class GuildMember extends Base { * The user that this guild member instance represents * @type {User} */ - this.user = {}; + if (data.user) this.user = client.users.add(data.user, true); /** * The timestamp the member joined the guild at @@ -79,6 +79,14 @@ class GuildMember extends Base { return clone; } + /** + * Whether this GuildMember is a partial + * @type {boolean} + */ + get partial() { + return !this.joinedTimestamp; + } + /** * A collection of roles that are applied to this member, mapped by the role ID * @type {GuildMemberRoleStore} @@ -355,6 +363,14 @@ class GuildMember extends Base { return this.guild.members.ban(this, options); } + /** + * Fetches this GuildMember. + * @returns {Promise} + */ + fetch() { + return this.guild.members.fetch(this.id, true); + } + /** * When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object. * @returns {string} diff --git a/src/structures/Message.js b/src/structures/Message.js index 310706e15..4b81cf94a 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -24,7 +24,7 @@ class Message extends Base { /** * The channel that the message was sent in - * @type {TextChannel|DMChannel|GroupDMChannel} + * @type {TextChannel|DMChannel} */ this.channel = channel; @@ -60,7 +60,7 @@ class Message extends Base { * The author of the message * @type {User} */ - this.author = this.client.users.add(data.author, !data.webhook_id); + this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null; /** * Whether or not this message is pinned @@ -90,17 +90,19 @@ class Message extends Base { * A list of embeds in the message - e.g. YouTube Player * @type {MessageEmbed[]} */ - this.embeds = data.embeds.map(e => new Embed(e)); + this.embeds = (data.embeds || []).map(e => new Embed(e)); /** * A collection of attachments in the message - e.g. Pictures - mapped by their ID * @type {Collection} */ this.attachments = new Collection(); - for (const attachment of data.attachments) { - this.attachments.set(attachment.id, new MessageAttachment( - attachment.url, attachment.filename, attachment - )); + if (data.attachments) { + for (const attachment of data.attachments) { + this.attachments.set(attachment.id, new MessageAttachment( + attachment.url, attachment.filename, attachment + )); + } } /** @@ -167,6 +169,14 @@ class Message extends Base { } } + /** + * Whether or not this message is a partial + * @type {boolean} + */ + get partial() { + return typeof this.content !== 'string' || !this.author; + } + /** * Updates the message. * @param {Object} data Raw Discord message update data @@ -472,6 +482,14 @@ class Message extends Base { ); } + /** + * Fetch this message. + * @returns {Promise} + */ + fetch() { + return this.channel.messages.fetch(this.id, true); + } + /** * Fetches the webhook used to create this message. * @returns {Promise} diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 59120b9db..44156cc03 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -15,7 +15,7 @@ const { Events } = require('../util/Constants'); */ class MessageCollector extends Collector { /** - * @param {TextChannel|DMChannel|GroupDMChannel} channel The channel + * @param {TextChannel|DMChannel} channel The channel * @param {CollectorFilter} filter The filter to be applied to this collector * @param {MessageCollectorOptions} options The options to be applied to this collector * @emits MessageCollector#message diff --git a/src/structures/User.js b/src/structures/User.js index 22ea5c81a..24bce7937 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -53,6 +53,8 @@ class User extends Base { */ if (typeof data.avatar !== 'undefined') this.avatar = data.avatar; + if (typeof data.bot !== 'undefined') this.bot = Boolean(data.bot); + /** * The locale of the user's client (ISO 639-1) * @type {?string} @@ -73,6 +75,14 @@ class User extends Base { this.lastMessageChannelID = null; } + /** + * Whether this User is a partial + * @type {boolean} + */ + get partial() { + return typeof this.username !== 'string'; + } + /** * The timestamp the user was created at * @type {number} @@ -228,6 +238,14 @@ class User extends Base { return equal; } + /** + * Fetches this user. + * @returns {Promise} + */ + fetch() { + return this.client.users.fetch(this.id, true); + } + /** * When concatenated with a string, this automatically returns the user's mention instead of the User object. * @returns {string} diff --git a/src/util/Constants.js b/src/util/Constants.js index 508b5f97d..1e13852cf 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,6 +21,9 @@ const browser = exports.browser = typeof window !== 'undefined'; * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) * @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone} + * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when + * they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some + * important usage information, as partials require you to put checks in place when handling data. * @property {number} [restWsBridgeTimeout=5000] Maximum time permitted between REST responses and their * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST @@ -44,6 +47,7 @@ exports.DefaultOptions = { messageSweepInterval: 0, fetchAllMembers: false, disableEveryone: false, + partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], retryLimit: 1, @@ -261,6 +265,23 @@ exports.Events = { RAW: 'raw', }; +/** + * The type of Structure allowed to be a partial: + * * USER + * * CHANNEL (only affects DMChannels) + * * GUILD_MEMBER + * * MESSAGE + * Partials require you to put checks in place when handling data, read the Partials topic listed in the + * sidebar for more information. + * @typedef {string} PartialType + */ +exports.PartialTypes = keyMirror([ + 'USER', + 'CHANNEL', + 'GUILD_MEMBER', + 'MESSAGE', +]); + /** * The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events: * * READY diff --git a/src/util/Structures.js b/src/util/Structures.js index a742e2920..4ac562606 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -67,7 +67,6 @@ class Structures { const structures = { GuildEmoji: require('../structures/GuildEmoji'), DMChannel: require('../structures/DMChannel'), - GroupDMChannel: require('../structures/GroupDMChannel'), TextChannel: require('../structures/TextChannel'), VoiceChannel: require('../structures/VoiceChannel'), CategoryChannel: require('../structures/CategoryChannel'), diff --git a/src/util/Util.js b/src/util/Util.js index 1d7b7aeeb..bbf838e46 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -395,7 +395,7 @@ class Util { .replace(/@(everyone|here)/g, '@\u200b$1') .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); - if (message.channel.type === 'dm' || message.channel.type === 'group') { + if (message.channel.type === 'dm') { const user = message.client.users.get(id); return user ? `@${user.username}` : input; } @@ -413,7 +413,7 @@ class Util { return channel ? `#${channel.name}` : input; }) .replace(/<@&[0-9]+>/g, input => { - if (message.channel.type === 'dm' || message.channel.type === 'group') return input; + if (message.channel.type === 'dm') return input; const role = message.guild.roles.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; }); diff --git a/test/voice.js b/test/voice.js index 70b5c30f7..6d022a29c 100644 --- a/test/voice.js +++ b/test/voice.js @@ -6,7 +6,7 @@ const ytdl = require('ytdl-core'); const prism = require('prism-media'); const fs = require('fs'); -const client = new Discord.Client({ fetchAllMembers: false, apiRequestMethod: 'sequential' }); +const client = new Discord.Client({ fetchAllMembers: false, partials: true, apiRequestMethod: 'sequential' }); const auth = require('./auth.js'); @@ -34,6 +34,15 @@ client.on('presenceUpdate', (a, b) => { console.log(a ? a.status : null, b.status, b.user.username); }); +client.on('messageDelete', async (m) => { + if (m.channel.id != '80426989059575808') return; + console.log(m.channel.recipient); + console.log(m.channel.partial); + await m.channel.fetch(); + console.log('\n\n\n\n'); + console.log(m.channel); +}); + client.on('message', m => { if (!m.guild) return; if (m.author.id !== '66564597481480192') return; diff --git a/typings/index.d.ts b/typings/index.d.ts index 8c606400c..4283755d1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -129,8 +129,9 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public deleted: boolean; public id: Snowflake; - public type: 'dm' | 'group' | 'text' | 'voice' | 'category' | 'unknown'; + public type: 'dm' | 'text' | 'voice' | 'category' | 'unknown'; public delete(reason?: string): Promise; + public fetch(): Promise; public toString(): string; } @@ -264,7 +265,6 @@ declare module 'discord.js' { export class ClientUser extends User { public mfaEnabled: boolean; public verified: boolean; - public createGroupDM(recipients: GroupDMRecipientOptions[]): Promise; public setActivity(options?: ActivityOptions): Promise; public setActivity(name: string, options?: ActivityOptions): Promise; public setAFK(afk: boolean): Promise; @@ -360,6 +360,7 @@ declare module 'discord.js' { constructor(client: Client, data?: object); public messages: MessageStore; public recipient: User; + public readonly partial: boolean; } export class Emoji extends Base { @@ -376,26 +377,6 @@ declare module 'discord.js' { public toString(): string; } - export class GroupDMChannel extends TextBasedChannel(Channel) { - constructor(client: Client, data?: object); - public applicationID: Snowflake; - public icon: string; - public managed: boolean; - public messages: MessageStore; - public name: string; - public nicks: Collection; - public readonly owner: User; - public ownerID: Snowflake; - public recipients: Collection; - public addUser(options: { user: UserResolvable, accessToken?: string, nick?: string }): Promise; - public edit (data: { icon?: string, name?: string }): Promise; - public equals(channel: GroupDMChannel): boolean; - public iconURL(options?: AvatarOptions): string; - public removeUser(user: UserResolvable): Promise; - public setIcon(icon: Base64Resolvable | BufferResolvable): Promise; - public setName(name: string): Promise; - } - export class Guild extends Base { constructor(client: Client, data: object); private _sortedRoles(): Collection; @@ -570,12 +551,14 @@ declare module 'discord.js' { public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string; + public readonly partial: boolean; public readonly permissions: Readonly; public readonly presence: Presence; public roles: GuildMemberRoleStore; public user: User; public readonly voice: VoiceState; public ban(options?: BanOptions): Promise; + public fetch(): Promise; public createDM(): Promise; public deleteDM(): Promise; public edit(data: GuildMemberEditData, reason?: string): Promise; @@ -619,7 +602,7 @@ declare module 'discord.js' { export class Invite extends Base { constructor(client: Client, data: object); - public channel: GuildChannel | GroupDMChannel; + public channel: GuildChannel; public code: string; public readonly createdAt: Date; public createdTimestamp: number; @@ -640,7 +623,7 @@ declare module 'discord.js' { } export class Message extends Base { - constructor(client: Client, data: object, channel: TextChannel | DMChannel | GroupDMChannel); + constructor(client: Client, data: object, channel: TextChannel | DMChannel); private _edits: Message[]; private patch(data: object): void; @@ -648,7 +631,7 @@ declare module 'discord.js' { public application: ClientApplication; public attachments: Collection; public author: User; - public channel: TextChannel | DMChannel | GroupDMChannel; + public channel: TextChannel | DMChannel; public readonly cleanContent: string; public content: string; public readonly createdAt: Date; @@ -665,6 +648,7 @@ declare module 'discord.js' { public readonly member: GuildMember; public mentions: MessageMentions; public nonce: string; + public readonly partial: boolean; public readonly pinnable: boolean; public pinned: boolean; public reactions: ReactionStore; @@ -680,6 +664,7 @@ declare module 'discord.js' { public edit(options: MessageEditOptions | MessageEmbed | APIMessage): Promise; public equals(message: Message, rawData: object): boolean; public fetchWebhook(): Promise; + public fetch(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; @@ -706,7 +691,7 @@ declare module 'discord.js' { } export class MessageCollector extends Collector { - constructor(channel: TextChannel | DMChannel | GroupDMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); + constructor(channel: TextChannel | DMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); public channel: Channel; public options: MessageCollectorOptions; public received: number; @@ -1078,6 +1063,7 @@ declare module 'discord.js' { public readonly dmChannel: DMChannel; public id: Snowflake; public locale: string; + public readonly partial: boolean; public readonly presence: Presence; public readonly tag: string; public username: string; @@ -1086,6 +1072,7 @@ declare module 'discord.js' { public deleteDM(): Promise; public displayAvatarURL(options?: AvatarOptions): string; public equals(user: User): boolean; + public fetch(): Promise; public toString(): string; public typingDurationIn(channel: ChannelResolvable): number; public typingIn(channel: ChannelResolvable): boolean; @@ -1385,7 +1372,7 @@ declare module 'discord.js' { } export class MessageStore extends DataStore { - constructor(channel: TextChannel | DMChannel | GroupDMChannel, iterable?: Iterable); + constructor(channel: TextChannel | DMChannel, iterable?: Iterable); public fetch(message: Snowflake): Promise; public fetch(options?: ChannelLogsQueryOptions): Promise>; public fetchPinned(): Promise>; @@ -1607,6 +1594,7 @@ declare module 'discord.js' { messageSweepInterval?: number; fetchAllMembers?: boolean; disableEveryone?: boolean; + partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; restSweepInterval?: number; @@ -1676,7 +1664,6 @@ declare module 'discord.js' { type Extendable = { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; - GroupDMChannel: typeof GroupDMChannel; TextChannel: typeof TextChannel; VoiceChannel: typeof VoiceChannel; CategoryChannel: typeof CategoryChannel; @@ -1711,13 +1698,6 @@ declare module 'discord.js' { type: number; }; - type GroupDMRecipientOptions = { - user?: UserResolvable | Snowflake; - accessToken?: string; - nick?: string; - id?: Snowflake; - }; - type GuildAuditLogsAction = keyof GuildAuditLogsActions; type GuildAuditLogsActions = { @@ -1934,7 +1914,7 @@ declare module 'discord.js' { type MessageResolvable = Message | Snowflake; - type MessageTarget = TextChannel | DMChannel | GroupDMChannel | User | GuildMember | Webhook | WebhookClient; + type MessageTarget = TextChannel | DMChannel | User | GuildMember | Webhook | WebhookClient; type MessageType = 'DEFAULT' | 'RECIPIENT_ADD' @@ -2023,6 +2003,11 @@ declare module 'discord.js' { desktop?: ClientPresenceStatus }; + type PartialTypes = 'USER' + | 'CHANNEL' + | 'GUILD_MEMBER' + | 'MESSAGE'; + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; From 4289b18ab27110504a78c2f8b2ad7800912a623e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 14 Feb 2019 11:19:05 +0100 Subject: [PATCH 0978/1359] docs(GuildMember): add missing @name to document user property Fixes #3090 --- src/structures/GuildMember.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index e2dea4322..91f54c8c9 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -27,6 +27,7 @@ class GuildMember extends Base { /** * The user that this guild member instance represents * @type {User} + * @name GuildMember#user */ if (data.user) this.user = client.users.add(data.user, true); From 0564c5c777cfd2391261417a2047d4d7f92474ca Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 14 Feb 2019 16:57:26 +0000 Subject: [PATCH 0979/1359] voice: update prism --- src/client/voice/util/PlayInterface.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index ac66eb290..873cd81e9 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -75,10 +75,10 @@ class PlayInterface { return this.player.playOpusStream(resource, options); } else if (type === 'ogg/opus') { if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); - return this.player.playOpusStream(resource.pipe(new prism.OggOpusDemuxer()), options); + return this.player.playOpusStream(resource.pipe(new prism.opus.OggDemuxer()), options); } else if (type === 'webm/opus') { if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM'); - return this.player.playOpusStream(resource.pipe(new prism.WebmOpusDemuxer()), options); + return this.player.playOpusStream(resource.pipe(new prism.opus.WebmDemuxer()), options); } } throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); From 4009986bc8bd1ccf0bdbd0b5135c1bde78839ed2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 17 Feb 2019 21:48:47 +0000 Subject: [PATCH 0980/1359] voice: destroy opus --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 93c6df81f..102c5eab7 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -117,7 +117,7 @@ class StreamDispatcher extends Writable { if (this.player.dispatcher === this) this.player.dispatcher = null; const { streams } = this; if (streams.broadcast) streams.broadcast.dispatchers.delete(this); - if (streams.opus) streams.opus.unpipe(this); + if (streams.opus) streams.opus.destroy(); if (streams.ffmpeg) streams.ffmpeg.destroy(); super._destroy(err, cb); } From 6aa792f9ab8008c22397d1a46a862fa88ba5aef5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 19 Feb 2019 13:16:48 +0000 Subject: [PATCH 0981/1359] voice: cleanup internals whether end() or destroy() is called --- src/client/voice/dispatcher/StreamDispatcher.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 102c5eab7..e05cd49ec 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -67,6 +67,7 @@ class StreamDispatcher extends Writable { this.count = 0; this.on('finish', () => { + this._cleanup(); // Still emitting end for backwards compatibility, probably remove it in the future! this.emit('end'); this._setSpeaking(0); @@ -114,12 +115,16 @@ class StreamDispatcher extends Writable { } _destroy(err, cb) { + this._cleanup(); + super._destroy(err, cb); + } + + _cleanup() { if (this.player.dispatcher === this) this.player.dispatcher = null; const { streams } = this; if (streams.broadcast) streams.broadcast.dispatchers.delete(this); if (streams.opus) streams.opus.destroy(); if (streams.ffmpeg) streams.ffmpeg.destroy(); - super._destroy(err, cb); } /** From efbbfbcec6d9b090eb7053bbb39acea50c46c517 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 20 Feb 2019 19:15:33 +0000 Subject: [PATCH 0982/1359] fix #3102 --- src/client/voice/player/BasePlayer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.js index 763762ef6..dc8f96027 100644 --- a/src/client/voice/player/BasePlayer.js +++ b/src/client/voice/player/BasePlayer.js @@ -66,8 +66,8 @@ class BasePlayer extends EventEmitter { stream.pipe(opus); return this.playOpusStream(opus, options, streams); } - const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 }); - stream.pipe(volume).pipe(opus); + streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); + stream.pipe(streams.volume).pipe(opus); return this.playOpusStream(opus, options, streams); } @@ -77,10 +77,10 @@ class BasePlayer extends EventEmitter { if (options.volume !== false && !streams.input) { streams.input = stream; const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 }); - const volume = streams.volume = new prism.VolumeTransformer16LE({ volume: options ? options.volume : 1 }); + streams.volume = new prism.VolumeTransformer({ type: 's16le', volume: options ? options.volume : 1 }); streams.opus = stream .pipe(decoder) - .pipe(volume) + .pipe(streams.volume) .pipe(new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 })); } const dispatcher = this.createDispatcher(options, streams); From 73be952406e54cb7626bc0f07085a32b7462126c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 21 Feb 2019 11:56:46 +0000 Subject: [PATCH 0983/1359] use official release of prism --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41385f9dc..351e5cb42 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "form-data": "^2.3.3", "node-fetch": "^2.3.0", "pako": "^1.0.8", - "prism-media": "amishshah/prism-media", + "prism-media": "^1.0.0", "setimmediate": "^1.0.5", "tweetnacl": "^1.0.1", "ws": "^6.1.3" From 4d3f76656a7456fb4f196379346860114a70f1f9 Mon Sep 17 00:00:00 2001 From: Linn Dahlgren Date: Sat, 23 Feb 2019 10:05:04 +0100 Subject: [PATCH 0984/1359] docs(MessageEmbed): add missing type value (#3106) --- src/structures/MessageEmbed.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 0c925d276..0b630cc68 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -16,6 +16,7 @@ class MessageEmbed { * The type of this embed, either: * * `image` - an image embed * * `video` - a video embed + * * `gifv` - a gifv embed * * `link` - a link embed * * `rich` - a rich embed * @type {string} From bc0a761e2242c55db91192fd0a3e438507354e21 Mon Sep 17 00:00:00 2001 From: Kyra Date: Sun, 24 Feb 2019 05:14:09 +0100 Subject: [PATCH 0985/1359] typings: Convert types to interfaces where applicable (#3068) --- typings/index.d.ts | 340 +++++++++++++++++++++++---------------------- 1 file changed, 173 insertions(+), 167 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4283755d1..6ac87dbb2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,11 +1,3 @@ -// Type definitions for discord.js 12.0.0 -// Project: https://github.com/hydrabolt/discord.js -// Definitions by: -// acdenisSK (https://github.com/acdenisSK) -// Zack Campbell (https://github.com/zajrik) -// iCrawl (https://github.com/iCrawl) -// License: MIT - declare module 'discord.js' { import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; @@ -72,7 +64,7 @@ declare module 'discord.js' { } export class Base { - constructor (client: Client); + constructor(client: Client); public readonly client: Client; public toJSON(...props: { [key: string]: boolean | string }[]): object; public valueOf(): string; @@ -837,7 +829,7 @@ declare module 'discord.js' { public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; public once(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; public once(event: string, listener: Function): this; -} + } export class ReactionEmoji extends Emoji { constructor(reaction: MessageReaction, emoji: object); @@ -1340,7 +1332,7 @@ declare module 'discord.js' { } // Hacky workaround because changing the signature of an overriden method errors - class OverridableDataStore, R = any> extends DataStore { + class OverridableDataStore, R = any> extends DataStore { public add(data: any, cache: any): any; public set(key: any): any; } @@ -1419,7 +1411,7 @@ declare module 'discord.js' { const PartialTextBasedChannel: (Base?: Constructable) => Constructable; const TextBasedChannel: (Base?: Constructable) => Constructable; - type PartialTextBasedChannelFields = { + interface PartialTextBasedChannelFields { lastMessageID: Snowflake; lastMessageChannelID: Snowflake; readonly lastMessage: Message; @@ -1427,9 +1419,9 @@ declare module 'discord.js' { readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - }; + } - type TextBasedChannelFields = { + interface TextBasedChannelFields extends PartialTextBasedChannelFields { typing: boolean; typingCount: number; awaitMessages(filter: CollectorFilter, options?: AwaitMessagesOptions): Promise>; @@ -1437,11 +1429,11 @@ declare module 'discord.js' { createMessageCollector(filter: CollectorFilter, options?: MessageCollectorOptions): MessageCollector; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; - } & PartialTextBasedChannelFields; + } const WebhookMixin: (Base?: Constructable) => Constructable; - type WebhookFields = { + interface WebhookFields { readonly client: Client; id: Snowflake; token: string; @@ -1449,21 +1441,26 @@ declare module 'discord.js' { edit(options: WebhookEditData): Promise; send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise; send(options?: WebhookMessageOptions | MessageAdditions | APIMessage): Promise; - sendSlackMessage(body: object): Promise; - }; + sendSlackMessage(body: object): Promise; + } //#endregion //#region Typedefs - type ActivityFlagsString = 'INSTANCE' | 'JOIN' | 'SPECTATE' | 'JOIN_REQUEST' | 'SYNC' | 'PLAY'; + type ActivityFlagsString = 'INSTANCE' + | 'JOIN' + | 'SPECTATE' + | 'JOIN_REQUEST' + | 'SYNC' + | 'PLAY'; type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' | 'WATCHING'; - type APIErrror = { + interface APIErrror { UNKNOWN_ACCOUNT: number; UNKNOWN_APPLICATION: number; UNKNOWN_CHANNEL: number; @@ -1508,35 +1505,39 @@ declare module 'discord.js' { BULK_DELETE_MESSAGE_TOO_OLD: number; INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: number; REACTION_BLOCKED: number; - }; + } - type AddGuildMemberOptions = { + interface AddGuildMemberOptions { accessToken: String; nick?: string; roles?: Collection | RoleResolvable[]; mute?: boolean; deaf?: boolean; - }; + } - type AuditLogChange = { + interface AuditLogChange { key: string; old?: any; new?: any; - }; + } - type AvatarOptions = { + interface AvatarOptions { format?: ImageExt; size?: ImageSize; - }; + } - type AwaitMessagesOptions = MessageCollectorOptions & { errors?: string[] }; + interface AwaitMessagesOptions extends MessageCollectorOptions { + errors?: string[]; + } - type AwaitReactionsOptions = ReactionCollectorOptions & { errors?: string[] }; + interface AwaitReactionsOptions extends ReactionCollectorOptions { + errors?: string[]; + } - type BanOptions = { + interface BanOptions { days?: number; reason?: string; - }; + } type Base64Resolvable = Buffer | Base64String; @@ -1546,13 +1547,13 @@ declare module 'discord.js' { type BufferResolvable = Buffer | string; - type ChannelCreationOverwrites = { + interface ChannelCreationOverwrites { allow?: PermissionResolvable | number; deny?: PermissionResolvable | number; id: RoleResolvable | UserResolvable; - }; + } - type ChannelData = { + interface ChannelData { name?: string; position?: number; topic?: string; @@ -1563,29 +1564,29 @@ declare module 'discord.js' { rateLimitPerUser?: number; lockPermissions?: boolean; permissionOverwrites?: OverwriteResolvable[] | Collection; - }; + } - type ChannelLogsQueryOptions = { - limit?: number - before?: Snowflake - after?: Snowflake - around?: Snowflake - }; + interface ChannelLogsQueryOptions { + limit?: number; + before?: Snowflake; + after?: Snowflake; + around?: Snowflake; + } - type ChannelPosition = { + interface ChannelPosition { channel: ChannelResolvable; position: number; - }; + } type ChannelResolvable = Channel | Snowflake; - type ClientApplicationAsset = { + interface ClientApplicationAsset { name: string; id: Snowflake; type: 'BIG' | 'SMALL'; - }; + } - type ClientOptions = { + interface ClientOptions { shards?: number | number[]; shardCount?: number; totalShardCount?: number; @@ -1603,15 +1604,16 @@ declare module 'discord.js' { disabledEvents?: WSEventType[]; ws?: WebSocketOptions; http?: HTTPOptions; - }; + } type CollectorFilter = (...args: any[]) => boolean; - type CollectorOptions = { + + interface CollectorOptions { time?: number; dispose?: boolean; - }; + } - type ColorResolvable = ('DEFAULT' + type ColorResolvable = 'DEFAULT' | 'AQUA' | 'GREEN' | 'BLUE' @@ -1634,34 +1636,38 @@ declare module 'discord.js' { | 'DARK_GREY' | 'LIGHT_GREY' | 'DARK_NAVY' - | 'RANDOM') + | 'RANDOM' | [number, number, number] | number | string; - type DeconstructedSnowflake = { + interface DeconstructedSnowflake { timestamp: number; readonly date: Date; workerID: number; processID: number; increment: number; binary: string; - }; + } type DefaultMessageNotifications = 'ALL' | 'MENTIONS'; - type GuildEmojiEditData = { + interface GuildEmojiEditData { name?: string; roles?: Collection | RoleResolvable[]; - }; + } - type EmbedField = { name: string, value: string, inline?: boolean }; + interface EmbedField { + name: string; + value: string; + inline?: boolean; + } type EmojiIdentifierResolvable = string | EmojiResolvable; type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; - type Extendable = { + interface Extendable { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; TextChannel: typeof TextChannel; @@ -1676,76 +1682,76 @@ declare module 'discord.js' { VoiceState: typeof VoiceState; Role: typeof Role; User: typeof User; - }; + } - type FetchMemberOptions = { + interface FetchMemberOptions { user: UserResolvable; cache?: boolean; - }; + } - type FetchMembersOptions = { + interface FetchMembersOptions { query?: string; limit?: number; - }; + } - type FileOptions = { + interface FileOptions { attachment: BufferResolvable | Stream; name?: string; - }; + } - type GroupActivity = { + interface GroupActivity { partyID: string; type: number; - }; + } type GuildAuditLogsAction = keyof GuildAuditLogsActions; - type GuildAuditLogsActions = { - ALL?: null, - GUILD_UPDATE?: number, - CHANNEL_CREATE?: number, - CHANNEL_UPDATE?: number, - CHANNEL_DELETE?: number, - CHANNEL_OVERWRITE_CREATE?: number, - CHANNEL_OVERWRITE_UPDATE?: number, - CHANNEL_OVERWRITE_DELETE?: number, - MEMBER_KICK?: number, - MEMBER_PRUNE?: number, - MEMBER_BAN_ADD?: number, - MEMBER_BAN_REMOVE?: number, - MEMBER_UPDATE?: number, - MEMBER_ROLE_UPDATE?: number, - ROLE_CREATE?: number, - ROLE_UPDATE?: number, - ROLE_DELETE?: number, - INVITE_CREATE?: number, - INVITE_UPDATE?: number, - INVITE_DELETE?: number, - WEBHOOK_CREATE?: number, - WEBHOOK_UPDATE?: number, - WEBHOOK_DELETE?: number, - EMOJI_CREATE?: number, - EMOJI_UPDATE?: number, - EMOJI_DELETE?: number, - MESSAGE_DELETE?: number - }; + interface GuildAuditLogsActions { + ALL?: null; + GUILD_UPDATE?: number; + CHANNEL_CREATE?: number; + CHANNEL_UPDATE?: number; + CHANNEL_DELETE?: number; + CHANNEL_OVERWRITE_CREATE?: number; + CHANNEL_OVERWRITE_UPDATE?: number; + CHANNEL_OVERWRITE_DELETE?: number; + MEMBER_KICK?: number; + MEMBER_PRUNE?: number; + MEMBER_BAN_ADD?: number; + MEMBER_BAN_REMOVE?: number; + MEMBER_UPDATE?: number; + MEMBER_ROLE_UPDATE?: number; + ROLE_CREATE?: number; + ROLE_UPDATE?: number; + ROLE_DELETE?: number; + INVITE_CREATE?: number; + INVITE_UPDATE?: number; + INVITE_DELETE?: number; + WEBHOOK_CREATE?: number; + WEBHOOK_UPDATE?: number; + WEBHOOK_DELETE?: number; + EMOJI_CREATE?: number; + EMOJI_UPDATE?: number; + EMOJI_DELETE?: number; + MESSAGE_DELETE?: number; + } type GuildAuditLogsActionType = 'CREATE' | 'DELETE' | 'UPDATE' | 'ALL'; - type GuildAuditLogsFetchOptions = { + interface GuildAuditLogsFetchOptions { before?: Snowflake | GuildAuditLogsEntry; after?: Snowflake | GuildAuditLogsEntry; limit?: number; user?: UserResolvable; type?: string | number; - }; + } type GuildAuditLogsTarget = keyof GuildAuditLogsTargets; - type GuildAuditLogsTargets = { + interface GuildAuditLogsTargets { ALL?: string; GUILD?: string; CHANNEL?: string; @@ -1755,11 +1761,11 @@ declare module 'discord.js' { WEBHOOK?: string; EMOJI?: string; MESSAGE?: string; - }; + } type GuildChannelResolvable = Snowflake | GuildChannel; - type GuildCreateChannelOptions = { + interface GuildCreateChannelOptions { permissionOverwrites?: OverwriteResolvable[] | Collection; topic?: string; type?: 'text' | 'voice' | 'category'; @@ -1770,18 +1776,18 @@ declare module 'discord.js' { rateLimitPerUser?: number; position?: number; reason?: string; - }; + } - type GuildChannelCloneOptions = GuildCreateChannelOptions & { + interface GuildChannelCloneOptions extends GuildCreateChannelOptions { name?: string; - }; + } - type GuildEmojiCreateOptions = { + interface GuildEmojiCreateOptions { roles?: Collection | RoleResolvable[]; reason?: string; - }; + } - type GuildEditData = { + interface GuildEditData { name?: string; region?: string; verificationLevel?: number; @@ -1793,12 +1799,12 @@ declare module 'discord.js' { icon?: Base64Resolvable; owner?: GuildMemberResolvable; splash?: Base64Resolvable; - }; + } - type GuildEmbedData = { + interface GuildEmbedData { enabled: boolean; channel?: GuildChannelResolvable; - }; + } type GuildFeatures = 'INVITE_SPLASH' | 'MORE_EMOJI' @@ -1806,30 +1812,30 @@ declare module 'discord.js' { | 'VIP_REGIONS' | 'VANITY_URL'; - type GuildMemberEditData = { + interface GuildMemberEditData { nick?: string; roles?: Collection | RoleResolvable[]; mute?: boolean; deaf?: boolean; channel?: ChannelResolvable; - }; + } type GuildMemberResolvable = GuildMember | UserResolvable; type GuildResolvable = Guild | Snowflake; - type GuildPruneMembersOptions = { + interface GuildPruneMembersOptions { days?: number; dry?: boolean; reason?: string; - }; + } - type HTTPOptions = { + interface HTTPOptions { version?: number; host?: string; cdn?: string; invite?: string; - }; + } type ImageExt = 'webp' | 'png' @@ -1845,45 +1851,45 @@ declare module 'discord.js' { | 1024 | 2048; - type IntegrationData = { + interface IntegrationData { id: string; type: string; - }; + } - type IntegrationEditData = { + interface IntegrationEditData { expireBehavior?: number; expireGracePeriod?: number; - }; + } - type IntegrationAccount = { + interface IntegrationAccount { id: string; name: string; - }; + } - type InviteOptions = { + interface InviteOptions { temporary?: boolean; maxAge?: number; maxUses?: number; unique?: boolean; reason?: string; - }; + } type InviteResolvable = string; - type MessageCollectorOptions = CollectorOptions & { + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; - }; + } type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; - type MessageEditOptions = { + interface MessageEditOptions { content?: string; embed?: MessageEmbedOptions | null; code?: string | boolean; - }; + } - type MessageEmbedOptions = { + interface MessageEmbedOptions { title?: string; description?: string; url?: string; @@ -1896,19 +1902,19 @@ declare module 'discord.js' { image?: { url?: string; proxy_url?: string; proxyURL?: string; height?: number; width?: number; }; video?: { url?: string; height?: number; width?: number; }; footer?: { text?: string; icon_url?: string; iconURL?: string; }; - }; + } - type MessageOptions = { + interface MessageOptions { tts?: boolean; nonce?: string; content?: string; - embed?: MessageEmbed | MessageEmbedOptions, + embed?: MessageEmbed | MessageEmbedOptions; disableEveryone?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; reply?: UserResolvable; - }; + } type MessageReactionResolvable = MessageReaction | Snowflake; @@ -1925,22 +1931,22 @@ declare module 'discord.js' { | 'PINS_ADD' | 'GUILD_MEMBER_JOIN'; - type OverwriteData = { + interface OverwriteData { allow?: PermissionResolvable; deny?: PermissionResolvable; id: GuildMemberResolvable | RoleResolvable; type?: OverwriteType; - }; + } type OverwriteResolvable = PermissionOverwrites | OverwriteData; type OverwriteType = 'member' | 'role'; - type PermissionFlags = Record; + interface PermissionFlags extends Record { } - type PermissionObject = Record; + interface PermissionObject extends Record { } - type PermissionOverwriteOption = { [k in PermissionString]?: boolean | null }; + interface PermissionOverwriteOption extends Partial> { } type PermissionString = 'CREATE_INSTANT_INVITE' | 'KICK_MEMBERS' @@ -1976,13 +1982,13 @@ declare module 'discord.js' { type PermissionResolvable = BitFieldResolvable; - type PermissionOverwriteOptions = { + interface PermissionOverwriteOptions { allow: PermissionResolvable; deny: PermissionResolvable; id: UserResolvable | RoleResolvable; - }; + } - type PresenceData = { + interface PresenceData { status?: PresenceStatusData; afk?: boolean; activity?: { @@ -1991,17 +1997,17 @@ declare module 'discord.js' { url?: string; }; shardID?: number | number[]; - }; + } type PresenceResolvable = Presence | UserResolvable | Snowflake; type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; - type ClientPresenceStatusData = { - web?: ClientPresenceStatus, - mobile?: ClientPresenceStatus, - desktop?: ClientPresenceStatus - }; + interface ClientPresenceStatusData { + web?: ClientPresenceStatus; + mobile?: ClientPresenceStatus; + desktop?: ClientPresenceStatus; + } type PartialTypes = 'USER' | 'CHANNEL' @@ -2012,41 +2018,41 @@ declare module 'discord.js' { type PresenceStatusData = ClientPresenceStatus | 'invisible'; - type RateLimitData = { + interface RateLimitData { timeout: number; limit: number; timeDifference: number; method: string; path: string; route: string; - }; + } - type RawOverwriteData = { + interface RawOverwriteData { id: Snowflake; allow: number; deny: number; type: OverwriteType; - }; + } - type ReactionCollectorOptions = CollectorOptions & { + interface ReactionCollectorOptions extends CollectorOptions { max?: number; maxEmojis?: number; maxUsers?: number; - }; + } - type ResolvedOverwriteOptions = { + interface ResolvedOverwriteOptions { allow: Permissions; deny: Permissions; - }; + } - type RoleData = { + interface RoleData { name?: string; color?: ColorResolvable; hoist?: boolean; position?: number; permissions?: PermissionResolvable; mentionable?: boolean; - }; + } type RoleResolvable = Role | string; @@ -2054,25 +2060,25 @@ declare module 'discord.js' { type Snowflake = string; - type SplitOptions = { + interface SplitOptions { maxLength?: number; char?: string; prepend?: string; append?: string; - }; + } type Status = number; - type StreamOptions = { + interface StreamOptions { type?: StreamType; seek?: number; volume?: number; passes?: number; plp?: number; fec?: boolean; - bitrate?: number | 'auto' + bitrate?: number | 'auto'; highWaterMark?: number; - }; + } type SpeakingString = 'SPEAKING' | 'SOUNDSHARE'; @@ -2084,14 +2090,14 @@ declare module 'discord.js' { type VoiceStatus = number; - type WebhookEditData = { + interface WebhookEditData { name?: string; avatar?: BufferResolvable; channel?: ChannelResolvable; reason?: string; - }; + } - type WebhookMessageOptions = { + interface WebhookMessageOptions { username?: string; avatarURL?: string; tts?: boolean; @@ -2101,12 +2107,12 @@ declare module 'discord.js' { files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; - }; + } - type WebSocketOptions = { + interface WebSocketOptions { large_threshold?: number; compress?: boolean; - }; + } type WSEventType = 'READY' | 'RESUMED' From 7006f00ae4f2c9e23ec4f31bf2d1de9ae4c98b90 Mon Sep 17 00:00:00 2001 From: Linn Dahlgren Date: Sun, 24 Feb 2019 09:27:57 +0100 Subject: [PATCH 0986/1359] feat(MessageEmbed): add missing proxyURL property to video (#3109) * Added missing property to MessageEmbed.video * Updated typings for MessageEmbed.video --- src/structures/MessageEmbed.js | 8 +++++++- typings/index.d.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 0b630cc68..8276cad77 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -100,11 +100,17 @@ class MessageEmbed { * The video of this embed (if there is one) * @type {?Object} * @property {string} url URL of this video + * @property {string} proxyURL ProxyURL for this video * @property {number} height Height of this video * @property {number} width Width of this video * @readonly */ - this.video = data.video; + this.video = data.video ? { + url: data.video.url, + proxyURL: data.video.proxy_url, + height: data.video.height, + width: data.video.width, + } : null; /** * The author of this embed (if there is one) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6ac87dbb2..9a306de85 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -713,7 +713,7 @@ declare module 'discord.js' { public title: string; public type: string; public url: string; - public readonly video: { url?: string; height?: number; width?: number }; + public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number }; public addBlankField(inline?: boolean): this; public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; From fe5563e15857efa79d25a8bd0f5c4878b0b64c1b Mon Sep 17 00:00:00 2001 From: bdistin Date: Sun, 24 Feb 2019 02:37:10 -0600 Subject: [PATCH 0987/1359] refactor(User): avoid invoking dmChannel getter more than once per method (#3108) also refactors to async methods --- src/structures/User.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/structures/User.js b/src/structures/User.js index 24bce7937..f510ee137 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -204,22 +204,24 @@ class User extends Base { * Creates a DM channel between the client and the user. * @returns {Promise} */ - createDM() { - if (this.dmChannel) return Promise.resolve(this.dmChannel); - return this.client.api.users(this.client.user.id).channels.post({ data: { + async createDM() { + const { dmChannel } = this; + if (dmChannel) return dmChannel; + const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, - } }) - .then(data => this.client.actions.ChannelCreate.handle(data).channel); + } }); + return this.client.actions.ChannelCreate.handle(data).channel; } /** * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. * @returns {Promise} */ - deleteDM() { - if (!this.dmChannel) return Promise.reject(new Error('USER_NO_DMCHANNEL')); - return this.client.api.channels(this.dmChannel.id).delete() - .then(data => this.client.actions.ChannelDelete.handle(data).channel); + async deleteDM() { + const { dmChannel } = this; + if (!dmChannel) throw new Error('USER_NO_DMCHANNEL'); + const data = await this.client.api.channels(dmChannel.id).delete(); + return this.client.actions.ChannelDelete.handle(data).channel; } /** From 579283dfe9c35284eaa5ec2a8c1e081c48a78377 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 2 Mar 2019 21:58:40 +0100 Subject: [PATCH 0988/1359] fix(Role): proper undefined check for data.permission when editing --- src/structures/Role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index cbae56969..87cca568a 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -172,7 +172,7 @@ class Role extends Base { * .catch(console.error); */ async edit(data, reason) { - if (data.permissions) data.permissions = Permissions.resolve(data.permissions); + if (typeof data.permissions !== 'undefined') data.permissions = Permissions.resolve(data.permissions); else data.permissions = this.permissions.bitfield; if (typeof data.position !== 'undefined') { await Util.setPosition(this, data.position, false, this.guild._sortedRoles(), From 1207c243d1d819b2a78230bb194e07f14d966ea9 Mon Sep 17 00:00:00 2001 From: Kamran Mackey Date: Mon, 4 Mar 2019 12:32:15 -0700 Subject: [PATCH 0989/1359] typings(Guild): add missing defaultRole property (#3126) * [typings] Fix missing defaultRole property under Guild. Signed-off-by: Kamran Mackey * [typings] Added missing readonly identifier. Signed-off-by: Kamran Mackey --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 9a306de85..e60dcfdf8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -386,6 +386,7 @@ declare module 'discord.js' { public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; + public readonly defaultRole: Role; public deleted: boolean; public embedEnabled: boolean; public emojis: GuildEmojiStore; From 132788937a61490b5003117006f9e28164c14608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Tue, 5 Mar 2019 17:18:11 +0100 Subject: [PATCH 0990/1359] src: add missing events in constants (#3124) * src: Add missing events in constants * fix: Restore 'use strict'; --- src/util/Constants.js | 3 +++ typings/index.d.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 1e13852cf..b6c1b0355 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -249,6 +249,7 @@ exports.Events = { USER_NOTE_UPDATE: 'userNoteUpdate', USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', + VOICE_SERVER_UPDATE: 'voiceServerUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', VOICE_BROADCAST_SUBSCRIBE: 'subscribe', VOICE_BROADCAST_UNSUBSCRIBE: 'unsubscribe', @@ -316,6 +317,7 @@ exports.PartialTypes = keyMirror([ * * PRESENCE_UPDATE * * VOICE_STATE_UPDATE * * TYPING_START + * * VOICE_STATE_UPDATE * * VOICE_SERVER_UPDATE * * WEBHOOKS_UPDATE * @typedef {string} WSEventType @@ -352,6 +354,7 @@ exports.WSEvents = keyMirror([ 'PRESENCE_UPDATE', 'VOICE_STATE_UPDATE', 'TYPING_START', + 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE', 'WEBHOOKS_UPDATE', ]); diff --git a/typings/index.d.ts b/typings/index.d.ts index e60dcfdf8..e1838dfc5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2146,6 +2146,7 @@ declare module 'discord.js' { | 'PRESENCE_UPDATE' | 'VOICE_STATE_UPDATE' | 'TYPING_START' + | 'VOICE_STATE_UPDATE' | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; From 1673b6f8f5bc53a30e2f2ef1123057d4e50c37c8 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 8 Mar 2019 10:57:59 -0600 Subject: [PATCH 0991/1359] fix(APIMessage): edit shouldn't remove content (#3129) * edit shouldn't remove content If undefined is passed to the api, content isn't removed in such a case where you are only editing the embed. * fix a related doc string * update typings * requested changes --- src/structures/APIMessage.js | 25 +++++++++++++++---------- src/structures/Message.js | 2 +- typings/index.d.ts | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 41f983839..e6d67b686 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -65,13 +65,18 @@ class APIMessage { /** * Makes the content of this message. - * @returns {string|string[]} + * @returns {?(string|string[])} */ makeContent() { // eslint-disable-line complexity const GuildMember = require('./GuildMember'); - // eslint-disable-next-line eqeqeq - let content = Util.resolveString(this.options.content == null ? '' : this.options.content); + let content; + if (this.options.content === null) { + content = ''; + } else if (typeof this.options.content !== 'undefined') { + content = Util.resolveString(this.options.content); + } + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const splitOptions = isSplit ? { ...this.options.split } : undefined; @@ -88,24 +93,24 @@ class APIMessage { if (content || mentionPart) { if (isCode) { const codeName = typeof this.options.code === 'string' ? this.options.code : ''; - content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content, true)}\n\`\`\``; + content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content || '', true)}\n\`\`\``; if (isSplit) { splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; } } else if (mentionPart) { - content = `${mentionPart}${content}`; + content = `${mentionPart}${content || ''}`; } const disableEveryone = typeof this.options.disableEveryone === 'undefined' ? this.target.client.options.disableEveryone : this.options.disableEveryone; if (disableEveryone) { - content = content.replace(/@(everyone|here)/g, '@\u200b$1'); + content = (content || '').replace(/@(everyone|here)/g, '@\u200b$1'); } if (isSplit) { - content = Util.splitMessage(content, splitOptions); + content = Util.splitMessage(content || '', splitOptions); } } @@ -275,7 +280,7 @@ class APIMessage { /** * Transforms the user-level arguments into a final options object. Passing a transformed options object alone into * this method will keep it the same, allowing for the reuse of the final options object. - * @param {StringResolvable} [content=''] Content to send + * @param {StringResolvable} [content] Content to send * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto transformed options * @param {boolean} [isWebhook=false] Whether or not to use WebhookMessageOptions as the result @@ -284,7 +289,7 @@ class APIMessage { static transformOptions(content, options, extra = {}, isWebhook = false) { if (!options && typeof content === 'object' && !(content instanceof Array)) { options = content; - content = ''; + content = undefined; } if (!options) { @@ -311,7 +316,7 @@ class APIMessage { /** * Creates an `APIMessage` from user-level arguments. * @param {MessageTarget} target Target to send to - * @param {StringResolvable} [content=''] Content to send + * @param {StringResolvable} [content] Content to send * @param {MessageOptions|WebhookMessageOptions|MessageAdditions} [options={}] Options to use * @param {MessageOptions|WebhookMessageOptions} [extra={}] - Extra options to add onto transformed options * @returns {MessageOptions|WebhookMessageOptions} diff --git a/src/structures/Message.js b/src/structures/Message.js index 4b81cf94a..e302de265 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -365,7 +365,7 @@ class Message extends Base { /** * Edits the content of the message. - * @param {StringResolvable|APIMessage} [content=''] The new content for the message + * @param {StringResolvable|APIMessage} [content] The new content for the message * @param {MessageEditOptions|MessageEmbed} [options] The options to provide * @returns {Promise} * @example diff --git a/typings/index.d.ts b/typings/index.d.ts index e1838dfc5..c1dfb86b0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -56,7 +56,7 @@ declare module 'discord.js' { isWebhook?: boolean ): MessageOptions | WebhookMessageOptions; - public makeContent(): string | string[]; + public makeContent(): string | string[] | undefined; public resolve(): Promise; public resolveData(): this; public resolveFiles(): Promise; From df1889ab4902416de2bf6b3c144d55e35dbf116f Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Wed, 13 Mar 2019 05:14:32 +1100 Subject: [PATCH 0992/1359] cleanup(Guild): removed fetchAuditLogs' "after" option (#3142) * Removed code and documentation for Guild.fetchAuditLogs "after" option * Also removed the option from typings --- src/structures/Guild.js | 3 --- typings/index.d.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index f99627a4c..9c44b1b8c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -566,7 +566,6 @@ class Guild extends Base { * Fetches audit logs for this guild. * @param {Object} [options={}] Options for fetching audit logs * @param {Snowflake|GuildAuditLogsEntry} [options.before] Limit to entries from before specified entry - * @param {Snowflake|GuildAuditLogsEntry} [options.after] Limit to entries from after specified entry * @param {number} [options.limit] Limit number of entries * @param {UserResolvable} [options.user] Only show entries involving this user * @param {AuditLogAction|number} [options.type] Only show entries involving this action type @@ -579,12 +578,10 @@ class Guild extends Base { */ fetchAuditLogs(options = {}) { if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; - if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; return this.client.api.guilds(this.id)['audit-logs'].get({ query: { before: options.before, - after: options.after, limit: options.limit, user_id: this.client.users.resolveID(options.user), action_type: options.type, diff --git a/typings/index.d.ts b/typings/index.d.ts index c1dfb86b0..3f9f222af 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1744,7 +1744,6 @@ declare module 'discord.js' { interface GuildAuditLogsFetchOptions { before?: Snowflake | GuildAuditLogsEntry; - after?: Snowflake | GuildAuditLogsEntry; limit?: number; user?: UserResolvable; type?: string | number; From e62833b5e1d0ee9314c58792b639b3dbb2ea705b Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 19 Mar 2019 19:59:45 +0100 Subject: [PATCH 0993/1359] docs: mark getters as @ readonly --- src/client/voice/VoiceBroadcast.js | 1 + src/client/voice/VoiceConnection.js | 1 + src/client/voice/dispatcher/StreamDispatcher.js | 5 +++++ src/client/voice/networking/VoiceWebSocket.js | 1 + src/client/voice/util/VolumeInterface.js | 7 ++++--- src/stores/GuildEmojiRoleStore.js | 1 + src/stores/GuildMemberRoleStore.js | 1 + src/structures/ClientUser.js | 2 +- src/structures/DMChannel.js | 1 + src/structures/GuildMember.js | 1 + src/structures/Message.js | 1 + src/structures/Presence.js | 2 ++ src/structures/User.js | 1 + src/structures/VoiceChannel.js | 1 + src/structures/VoiceState.js | 5 +++++ 15 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 602f59205..27c8f7e28 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -34,6 +34,7 @@ class VoiceBroadcast extends EventEmitter { /** * The current master dispatcher, if any. This dispatcher controls all that is played by subscribed dispatchers. * @type {?BroadcastDispatcher} + * @readonly */ get dispatcher() { return this.player.dispatcher; diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index d0922ae81..727f21968 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -130,6 +130,7 @@ class VoiceConnection extends EventEmitter { /** * The client that instantiated this connection * @type {Client} + * @readonly */ get client() { return this.voiceManager.client; diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index e05cd49ec..71e6e1611 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -146,12 +146,14 @@ class StreamDispatcher extends Writable { /** * Whether or not playback is paused * @type {boolean} + * @readonly */ get paused() { return Boolean(this.pausedSince); } /** * Total time that this dispatcher has been paused * @type {number} + * @readonly */ get pausedTime() { return this._silentPausedTime + this._pausedTime + (this.paused ? Date.now() - this.pausedSince : 0); @@ -177,6 +179,7 @@ class StreamDispatcher extends Writable { /** * The time (in milliseconds) that the dispatcher has actually been playing audio for * @type {number} + * @readonly */ get streamTime() { return this.count * FRAME_LENGTH; @@ -185,6 +188,7 @@ class StreamDispatcher extends Writable { /** * The time (in milliseconds) that the dispatcher has been playing audio for, taking into account skips and pauses * @type {number} + * @readonly */ get totalStreamTime() { return Date.now() - this.startTime; @@ -322,6 +326,7 @@ class StreamDispatcher extends Writable { /** * Whether or not the Opus bitrate of this stream is editable * @type {boolean} + * @readonly */ get bitrateEditable() { return this.streams.opus && this.streams.opus.setBitrate; } diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 3720afa74..8f1e8340d 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -32,6 +32,7 @@ class VoiceWebSocket extends EventEmitter { /** * The client of this voice WebSocket * @type {Client} + * @readonly */ get client() { return this.connection.voiceManager.client; diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index a631e6caf..ba162a947 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -15,6 +15,7 @@ class VolumeInterface extends EventEmitter { /** * Whether or not the volume of this stream is editable * @type {boolean} + * @readonly */ get volumeEditable() { return true; @@ -22,8 +23,8 @@ class VolumeInterface extends EventEmitter { /** * The current volume of the stream - * @readonly * @type {number} + * @readonly */ get volume() { return this._volume; @@ -31,8 +32,8 @@ class VolumeInterface extends EventEmitter { /** * The current volume of the stream in decibels - * @readonly * @type {number} + * @readonly */ get volumeDecibels() { return Math.log10(this.volume) * 20; @@ -40,8 +41,8 @@ class VolumeInterface extends EventEmitter { /** * The current volume of the stream from a logarithmic scale - * @readonly * @type {number} + * @readonly */ get volumeLogarithmic() { return Math.pow(this.volume, 1 / 1.660964); diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index 8c097f564..2051161d9 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -20,6 +20,7 @@ class GuildEmojiRoleStore extends Collection { * The filtered collection of roles of the guild emoji * @type {Collection} * @private + * @readonly */ get _filtered() { return this.guild.roles.filter(role => this.emoji._roles.includes(role.id)); diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 9ab340d68..7b44a3536 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -20,6 +20,7 @@ class GuildMemberRoleStore extends Collection { * The filtered collection of roles of the member * @type {Collection} * @private + * @readonly */ get _filtered() { const everyone = this.guild.defaultRole; diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index a86a6cb56..20bc42b9f 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -30,8 +30,8 @@ class ClientUser extends Structures.get('User') { /** * ClientUser's presence - * @readonly * @type {Presence} + * @readonly */ get presence() { return this.client.presence; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index f1280e802..e58942c3e 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -49,6 +49,7 @@ class DMChannel extends Channel { /** * Whether this DMChannel is a partial * @type {boolean} + * @readonly */ get partial() { return !this.recipient; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 91f54c8c9..d479dd772 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -83,6 +83,7 @@ class GuildMember extends Base { /** * Whether this GuildMember is a partial * @type {boolean} + * @readonly */ get partial() { return !this.joinedTimestamp; diff --git a/src/structures/Message.js b/src/structures/Message.js index e302de265..afdb5e2c4 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -172,6 +172,7 @@ class Message extends Base { /** * Whether or not this message is a partial * @type {boolean} + * @readonly */ get partial() { return typeof this.content !== 'string' || !this.author; diff --git a/src/structures/Presence.js b/src/structures/Presence.js index baacab138..5d82ac07e 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -45,6 +45,7 @@ class Presence { /** * The user of this presence * @type {?User} + * @readonly */ get user() { return this.client.users.get(this.userID) || null; @@ -53,6 +54,7 @@ class Presence { /** * The member of this presence * @type {?GuildMember} + * @readonly */ get member() { return this.guild.members.get(this.userID) || null; diff --git a/src/structures/User.js b/src/structures/User.js index f510ee137..a0436f7e8 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -78,6 +78,7 @@ class User extends Base { /** * Whether this User is a partial * @type {boolean} + * @readonly */ get partial() { return typeof this.username !== 'string'; diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 2d1fa956e..8b49715fd 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -30,6 +30,7 @@ class VoiceChannel extends GuildChannel { * The members in this voice channel * @type {Collection} * @name VoiceChannel#members + * @readonly */ get members() { const coll = new Collection(); diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 82f9eec71..9ff71e87a 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -58,6 +58,7 @@ class VoiceState extends Base { /** * The member that this voice state belongs to * @type {?GuildMember} + * @readonly */ get member() { return this.guild.members.get(this.id) || null; @@ -66,6 +67,7 @@ class VoiceState extends Base { /** * The channel that the member is connected to * @type {?VoiceChannel} + * @readonly */ get channel() { return this.guild.channels.get(this.channelID) || null; @@ -74,6 +76,7 @@ class VoiceState extends Base { /** * Whether this member is either self-deafened or server-deafened * @type {?boolean} + * @readonly */ get deaf() { return this.serverDeaf || this.selfDeaf; @@ -82,6 +85,7 @@ class VoiceState extends Base { /** * Whether this member is either self-muted or server-muted * @type {?boolean} + * @readonly */ get mute() { return this.serverMute || this.selfMute; @@ -91,6 +95,7 @@ class VoiceState extends Base { * Whether this member is currently speaking. A boolean if the information is available (aka * the bot is connected to any voice channel in the guild), otherwise this is null * @type {?boolean} + * @readonly */ get speaking() { return this.channel && this.channel.connection ? From 2341d13615fb0a4649ba719c072661b65a4927a5 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 19 Mar 2019 19:32:11 +0000 Subject: [PATCH 0994/1359] fix: messageReactionRemove not emitting for partial messages (#3125) --- src/client/actions/Action.js | 13 +++++++++++++ src/client/actions/MessageReactionAdd.js | 9 +++++++++ src/client/actions/MessageReactionRemove.js | 3 +-- .../websocket/handlers/MESSAGE_REACTION_ADD.js | 11 +---------- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 09faae9e2..0c798089b 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -43,6 +43,19 @@ class GenericAction { }) : channel.messages.get(id)); } + + getReaction(data, message, user) { + const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); + const existing = message.reactions.get(emojiID); + if (!existing && this.client.options.partials.includes(PartialTypes.MESSAGE)) { + return message.reactions.add({ + emoji: data.emoji, + count: 0, + me: user.id === this.client.user.id, + }); + } + return existing; + } } module.exports = GenericAction; diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index b7400cf77..185b980ad 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -1,6 +1,7 @@ 'use strict'; const Action = require('./Action'); +const { Events } = require('../../util/Constants'); /* { user_id: 'id', @@ -31,6 +32,14 @@ class MessageReactionAdd extends Action { me: user.id === this.client.user.id, }); reaction._add(user); + /** + * Emitted whenever a reaction is added to a cached message. + * @event Client#messageReactionAdd + * @param {MessageReaction} messageReaction The reaction object + * @param {User} user The user that applied the guild or reaction emoji + */ + this.client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); + return { message, reaction, user }; } } diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index e5d4f8413..4e7995f8e 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -26,8 +26,7 @@ class MessageReactionRemove extends Action { if (!message) return false; // Verify reaction - const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); - const reaction = message.reactions.get(emojiID); + const reaction = this.getReaction(data, message, user); if (!reaction) return false; reaction._remove(user); /** diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js index 6d0bbb059..e219b4a52 100644 --- a/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js +++ b/src/client/websocket/handlers/MESSAGE_REACTION_ADD.js @@ -1,14 +1,5 @@ 'use strict'; -const { Events } = require('../../../util/Constants'); - module.exports = (client, packet) => { - const { user, reaction } = client.actions.MessageReactionAdd.handle(packet.d); - /** - * Emitted whenever a reaction is added to a cached message. - * @event Client#messageReactionAdd - * @param {MessageReaction} messageReaction The reaction object - * @param {User} user The user that applied the guild or reaction emoji - */ - if (reaction) client.emit(Events.MESSAGE_REACTION_ADD, reaction, user); + client.actions.MessageReactionAdd.handle(packet.d); }; From 9b2bf03ff6041148a0c6c6351a2bd74c44b4166a Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 19 Mar 2019 19:35:58 +0000 Subject: [PATCH 0995/1359] feat(MessageStore): add cache parameter to fetchPinned() (#3154) * add cache param to MessageStore#fetchPinned() * typings for cache param * set cache to true by default --- src/stores/MessageStore.js | 5 +++-- typings/index.d.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index 52bf7fdd6..db72114f6 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -66,6 +66,7 @@ class MessageStore extends DataStore { * Fetches the pinned messages of this channel and returns a collection of them. * The returned Collection does not contain any reaction data of the messages. * Those need to be fetched separately. + * @param {boolean} [cache=true] Whether to cache the message(s) * @returns {Promise>} * @example * // Get pinned messages @@ -73,10 +74,10 @@ class MessageStore extends DataStore { * .then(messages => console.log(`Received ${messages.size} messages`)) * .catch(console.error); */ - fetchPinned() { + fetchPinned(cache = true) { return this.client.api.channels[this.channel.id].pins.get().then(data => { const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message)); + for (const message of data) messages.set(message.id, this.add(message, cache)); return messages; }); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 3f9f222af..e1a6bac84 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1366,9 +1366,9 @@ declare module 'discord.js' { export class MessageStore extends DataStore { constructor(channel: TextChannel | DMChannel, iterable?: Iterable); - public fetch(message: Snowflake): Promise; - public fetch(options?: ChannelLogsQueryOptions): Promise>; - public fetchPinned(): Promise>; + public fetch(message: Snowflake, cache?: boolean): Promise; + public fetch(options?: ChannelLogsQueryOptions, cache?: boolean): Promise>; + public fetchPinned(cache?: boolean): Promise>; } export class PresenceStore extends DataStore { From 3df56540e2ca4adbd4ae018b2e296aeeb5bca0c3 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:22:55 +0000 Subject: [PATCH 0996/1359] voice: add documentation to VoiceBroadcast --- src/client/voice/VoiceBroadcast.js | 4 ++++ src/client/voice/dispatcher/BroadcastDispatcher.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 27c8f7e28..870798dd8 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -27,6 +27,10 @@ class VoiceBroadcast extends EventEmitter { * @type {Client} */ this.client = client; + /** + * The dispatchers playing this broadcast + * @type {Set} + */ this.dispatchers = new DispatcherSet(this); this.player = new BroadcastAudioPlayer(this); } diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index e5c7650cf..d7fd8dfaf 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -29,6 +29,12 @@ class BroadcastDispatcher extends StreamDispatcher { super._destroy(err, cb); } + /** + * Set the bitrate of the current Opus encoder if using a compatible Opus stream. + * @param {number} value New bitrate, in kbps + * If set to 'auto', 48kbps will be used + * @returns {boolean} true if the bitrate has been successfully changed. + */ setBitrate(value) { if (!value || !this.streams.opus || !this.streams.opus.setBitrate) return false; const bitrate = value === 'auto' ? 48 : value; From c8225631c9f7906c0aa4966db701fe594074f27c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:36:53 +0000 Subject: [PATCH 0997/1359] voice: move broadcasts to client.voice --- src/client/Client.js | 17 --------- src/client/voice/ClientVoiceManager.js | 20 ++++++++++- src/client/voice/VoiceBroadcast.js | 35 +++++++++++++++++-- .../voice/dispatcher/StreamDispatcher.js | 2 +- src/client/voice/player/AudioPlayer.js | 2 +- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index b1dc15573..10272aeee 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -144,12 +144,6 @@ class Client extends BaseClient { */ this.readyAt = null; - /** - * Active voice broadcasts that have been created - * @type {VoiceBroadcast[]} - */ - this.broadcasts = []; - if (this.options.messageSweepInterval > 0) { this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); } @@ -196,16 +190,6 @@ class Client extends BaseClient { return this.readyAt ? Date.now() - this.readyAt : null; } - /** - * Creates a voice broadcast. - * @returns {VoiceBroadcast} - */ - createVoiceBroadcast() { - const broadcast = new VoiceBroadcast(this); - this.broadcasts.push(broadcast); - return broadcast; - } - /** * Logs the client in, establishing a websocket connection to Discord. * @param {string} token Token of the account to log in with @@ -390,7 +374,6 @@ class Client extends BaseClient { toJSON() { return super.toJSON({ readyAt: false, - broadcasts: false, presences: false, }); } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 466b6d664..ba1e2a253 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -3,10 +3,11 @@ const Collection = require('../../util/Collection'); const { VoiceStatus } = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); +const VoiceBroadcast = require('./VoiceBroadcast'); const { Error } = require('../../errors'); /** - * Manages all the voice stuff for the client. + * Manages voice connections for the client * @private */ class ClientVoiceManager { @@ -22,6 +23,22 @@ class ClientVoiceManager { * @type {Collection} */ this.connections = new Collection(); + + /** + * Active voice broadcasts that have been created + * @type {VoiceBroadcast[]} + */ + this.broadcasts = []; + } + + /** + * Creates a voice broadcast. + * @returns {VoiceBroadcast} + */ + createVoiceBroadcast() { + const broadcast = new VoiceBroadcast(this); + this.broadcasts.push(broadcast); + return broadcast; } onVoiceServer({ guild_id, token, endpoint }) { @@ -46,6 +63,7 @@ class ClientVoiceManager { * Sets up a request to join a voice channel. * @param {VoiceChannel} channel The voice channel to join * @returns {Promise} + * @private */ joinChannel(channel) { return new Promise((resolve, reject) => { diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 870798dd8..6cb2bcd14 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -2,7 +2,7 @@ const EventEmitter = require('events'); const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer'); -const DispatcherSet = require('./util/DispatcherSet'); +const { Events } = require('../../util/Constants'); const PlayInterface = require('./util/PlayInterface'); /** @@ -29,9 +29,9 @@ class VoiceBroadcast extends EventEmitter { this.client = client; /** * The dispatchers playing this broadcast - * @type {Set} + * @type {StreamDispatcher[]} */ - this.dispatchers = new DispatcherSet(this); + this.dispatchers = []; this.player = new BroadcastAudioPlayer(this); } @@ -60,6 +60,35 @@ class VoiceBroadcast extends EventEmitter { * @returns {BroadcastDispatcher} */ play() { return null; } + + add(dispatcher) { + const index = this.dispatchers.indexOf(dispatcher); + if (index === -1) { + /** + * Emitted whenever a stream dispatcher subscribes to the broadcast. + * @event VoiceBroadcast#subscribe + * @param {StreamDispatcher} dispatcher The subscribed dispatcher + */ + this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); + return true; + } else { + return false; + } + } + + delete(dispatcher) { + const index = this.dispatchers.indexOf(dispatcher); + if (index !== -1) { + /** + * Emitted whenever a stream dispatcher unsubscribes to the broadcast. + * @event VoiceBroadcast#unsubscribe + * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher + */ + this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); + return true; + } + return false; + } } PlayInterface.applyToClass(VoiceBroadcast); diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 71e6e1611..340e40c5d 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -122,7 +122,7 @@ class StreamDispatcher extends Writable { _cleanup() { if (this.player.dispatcher === this) this.player.dispatcher = null; const { streams } = this; - if (streams.broadcast) streams.broadcast.dispatchers.delete(this); + if (streams.broadcast) streams.broadcast.delete(this); if (streams.opus) streams.opus.destroy(); if (streams.ffmpeg) streams.ffmpeg.destroy(); } diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 3ce94d815..6f719a730 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -19,7 +19,7 @@ class AudioPlayer extends BasePlayer { playBroadcast(broadcast, options) { const dispatcher = this.createDispatcher(options, { broadcast }); - broadcast.dispatchers.add(dispatcher); + broadcast.add(dispatcher); return dispatcher; } } From 9a092b6e57908e763c67a07dcbab5826f6bceef7 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:37:55 +0000 Subject: [PATCH 0998/1359] voice: rename createVoiceBroadcast to createBroadcast --- src/client/voice/ClientVoiceManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index ba1e2a253..a8bf79924 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -35,7 +35,7 @@ class ClientVoiceManager { * Creates a voice broadcast. * @returns {VoiceBroadcast} */ - createVoiceBroadcast() { + createBroadcast() { const broadcast = new VoiceBroadcast(this); this.broadcasts.push(broadcast); return broadcast; From 6adb0a6609905318959a51f4e9d16b2c2a503987 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:52:18 +0000 Subject: [PATCH 0999/1359] voice: add ability to delete broadcasts --- src/client/voice/ClientVoiceManager.js | 1 - src/client/voice/VoiceBroadcast.js | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index a8bf79924..fa3513643 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -8,7 +8,6 @@ const { Error } = require('../../errors'); /** * Manages voice connections for the client - * @private */ class ClientVoiceManager { constructor(client) { diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 6cb2bcd14..5446b46cc 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -61,6 +61,16 @@ class VoiceBroadcast extends EventEmitter { */ play() { return null; } + + /** + * Ends the broadcast, unsubscribing all subscribed channels and deleting the broadcast + */ + end() { + for (const dispatcher of this.dispatchers) this.delete(dispatcher); + const index = this.client.voice.broadcasts.indexOf(this); + if (index !== -1) this.client.voice.broadcasts.splice(index, 1); + } + add(dispatcher) { const index = this.dispatchers.indexOf(dispatcher); if (index === -1) { @@ -79,6 +89,8 @@ class VoiceBroadcast extends EventEmitter { delete(dispatcher) { const index = this.dispatchers.indexOf(dispatcher); if (index !== -1) { + this.dispatchers.splice(index, 1); + dispatcher.destroy(); /** * Emitted whenever a stream dispatcher unsubscribes to the broadcast. * @event VoiceBroadcast#unsubscribe From b74a4356dd91ec3d3d54da09fdb4328ae4d2041c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 23 Mar 2019 12:54:22 +0000 Subject: [PATCH 1000/1359] fix lint errors --- src/client/Client.js | 1 - src/client/voice/util/DispatcherSet.js | 42 -------------------------- 2 files changed, 43 deletions(-) delete mode 100644 src/client/voice/util/DispatcherSet.js diff --git a/src/client/Client.js b/src/client/Client.js index 10272aeee..06b8fb489 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -11,7 +11,6 @@ const Webhook = require('../structures/Webhook'); const Invite = require('../structures/Invite'); const ClientApplication = require('../structures/ClientApplication'); const ShardClientUtil = require('../sharding/ShardClientUtil'); -const VoiceBroadcast = require('./voice/VoiceBroadcast'); const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); diff --git a/src/client/voice/util/DispatcherSet.js b/src/client/voice/util/DispatcherSet.js deleted file mode 100644 index 38a7c8b6a..000000000 --- a/src/client/voice/util/DispatcherSet.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const { Events } = require('../../../util/Constants'); - -/** - * A "store" for handling broadcast dispatcher (un)subscription - * @private - */ -class DispatcherSet extends Set { - constructor(broadcast) { - super(); - /** - * The broadcast that this set belongs to - * @type {VoiceBroadcast} - */ - this.broadcast = broadcast; - } - - add(dispatcher) { - super.add(dispatcher); - /** - * Emitted whenever a stream dispatcher subscribes to the broadcast. - * @event VoiceBroadcast#subscribe - * @param {StreamDispatcher} dispatcher The subscribed dispatcher - */ - this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); - return this; - } - - delete(dispatcher) { - const ret = super.delete(dispatcher); - /** - * Emitted whenever a stream dispatcher unsubscribes to the broadcast. - * @event VoiceBroadcast#unsubscribe - * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher - */ - if (ret) this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); - return ret; - } -} - -module.exports = DispatcherSet; From 04fa56d0c07b66c77d0fdd6cb4419efb890043eb Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sun, 24 Mar 2019 16:45:34 -0400 Subject: [PATCH 1001/1359] Improve structure extension errors --- src/util/Structures.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 4ac562606..62646b404 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -45,17 +45,22 @@ class Structures { if (typeof extender !== 'function') { const received = `(received ${typeof extender})`; throw new TypeError( - `"extender" argument must be a function that returns the extended structure class/prototype ${received}` + `"extender" argument must be a function that returns the extended structure class/prototype ${received}.` ); } const extended = extender(structures[structure]); if (typeof extended !== 'function') { - throw new TypeError('The extender function must return the extended structure class/prototype.'); + const received = `(received ${typeof extended})`; + throw new TypeError(`The extender function must return the extended structure class/prototype ${received}.`); } - if (Object.getPrototypeOf(extended) !== structures[structure]) { + + const prototype = Object.getPrototypeOf(extended); + if (prototype !== structures[structure]) { + const received = `${extended.name || 'unnamed'}${prototype.name ? ` extends ${prototype.name}` : ''}`; throw new Error( - 'The class/prototype returned from the extender function must extend the existing structure class/prototype.' + 'The class/prototype returned from the extender function must extend the existing structure class/prototype' + + ` (received function ${received}; expected extension of ${structures[structure].name}).` ); } From 4b6e8fcab5324392d9b91822006c05d2f4819a40 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Mon, 1 Apr 2019 18:36:03 +1100 Subject: [PATCH 1002/1359] typings(Collection): add missing thisArg to partition (#3167) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e1a6bac84..f77ab51ee 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -289,7 +289,7 @@ declare module 'discord.js' { public lastKey(): K | undefined; public lastKey(count: number): K[]; public map(fn: (value: V, key: K, collection: Collection) => T, thisArg?: any): T[]; - public partition(fn: (value: V, key: K, collection: Collection) => boolean): [Collection, Collection]; + public partition(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): [Collection, Collection]; public random(): V | undefined; public random(count: number): V[]; public randomKey(): K | undefined; From 3f5161eb76b71ab8984e39ff39bf2937b82f5b43 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 1 Apr 2019 10:43:45 +0300 Subject: [PATCH 1003/1359] =?UTF-8?q?fix:=20Internal=20Sharding,=20this=20?= =?UTF-8?q?time=20fixed=E2=84=A2=20(#3140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * src: WIP Internal Sharding refactor * src: Refactor unavailable guild check Co-Authored-By: kyranet * src: More WIP Code F in the chat to the old manager * src: It should work but Discord says no. Seriously why is this not working! * fix: Inflator causing issues * src: Finishing touches and typings * misc: Proper debug message * fix: Making things hidden needs writable: true as well * fix: Sharding allowing multiple of the same shard, negative shards or strings * fix: Again... edge cases I love you guys .w. * misc: Touchups * misc: Better error? * docs: Typo * typings: Requested changes * src: Requested changes * src: Fix issues, validate provided shard options and more * src: Forgot to remove the listener * lint: eslint complaining * fix: Setting shardCount to auto crashing the process * misc: Requested changes * typings: Correct typings for shardCount client option * typings: Add invalidSession event to the shard and correct typings * src: Minor docs adjustements, and code consistency between setHelloTimeout and setHeartbeatTimeout * src: Don't block reconnect while creating shards Might fix silent disconnects *again* * src: Prevent reconnect from running if the Manager isn't READY That way, if a shard dies while we're still spawning, it won't cause issues * fix: Retry to reconnect if there's a network error going on. The manager *should* keep reconnecting unless the token is invalid * src: Enhance onClose handler for shards in the manager - If the close code is between 1000 and 2000 (inclusive), you cannot resume I tested this locally - If there's a session ID still present, immediately try to resume Faster resumes :papaBless: Otherwise, the invalid session event will trigger and it'll handle accordingly I swear if I see a SINGULAR Silent DC I'm yeeting * src: Fix error check * src: Make sure message exists on the error * src: Used the wrong property for the shardQueue * src: Make the hello timeout be made by the client god help * docs: Correct docs for WSEvents * misc: Remove old events from the Events constant * src: Throw the HTTP error if we don't get a 401 * typings: Can't forget about them * src: Implement some more fail safes just in case Seriously, better safe than sorry! Gotta failproof it completely --- package.json | 1 + src/client/Client.js | 80 ++-- src/client/websocket/WebSocketManager.js | 379 +++++++++++------ src/client/websocket/WebSocketShard.js | 495 +++++++++++++---------- src/errors/Messages.js | 2 +- src/rest/DiscordAPIError.js | 8 +- src/rest/RequestHandler.js | 2 +- src/util/Constants.js | 20 +- typings/index.d.ts | 97 ++++- 9 files changed, 671 insertions(+), 413 deletions(-) diff --git a/package.json b/package.json index 351e5cb42..fc5d8d1e0 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "devDependencies": { "@types/node": "^10.12.24", + "@types/ws": "^6.0.1", "discord.js-docgen": "discordjs/docgen", "eslint": "^5.13.0", "json-filter-loader": "^1.0.0", diff --git a/src/client/Client.js b/src/client/Client.js index 06b8fb489..73767f696 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -15,8 +15,7 @@ const UserStore = require('../stores/UserStore'); const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); -const { Events, WSCodes, browser, DefaultOptions } = require('../util/Constants'); -const { delayFor } = require('../util/Util'); +const { Events, browser, DefaultOptions } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const Structures = require('../util/Structures'); const { Error, TypeError, RangeError } = require('../errors'); @@ -40,23 +39,33 @@ class Client extends BaseClient { } catch (_) { // Do nothing } + if (this.options.shards === DefaultOptions.shards) { if ('SHARDS' in data) { this.options.shards = JSON.parse(data.SHARDS); } } + if (this.options.totalShardCount === DefaultOptions.totalShardCount) { if ('TOTAL_SHARD_COUNT' in data) { this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); - } else if (Array.isArray(this.options.shards)) { + } else if (this.options.shards instanceof Array) { this.options.totalShardCount = this.options.shards.length; } else { this.options.totalShardCount = this.options.shardCount; } } - if (typeof this.options.shards === 'undefined' && this.options.shardCount) { - this.options.shards = []; - for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); + + if (typeof this.options.shards === 'undefined' && typeof this.options.shardCount === 'number') { + this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); + } + + if (typeof this.options.shards === 'number') this.options.shards = [this.options.shards]; + + if (typeof this.options.shards !== 'undefined') { + this.options.shards = [...new Set( + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity) + )]; } this._validateOptions(); @@ -199,55 +208,21 @@ class Client extends BaseClient { async login(token = this.token) { if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); - this.emit(Events.DEBUG, `Authenticating using token ${token}`); - let endpoint = this.api.gateway; - if (this.options.shardCount === 'auto') endpoint = endpoint.bot; - const res = await endpoint.get(); + this.emit(Events.DEBUG, `Provided token: ${token}`); + if (this.options.presence) { this.options.ws.presence = await this.presence._parse(this.options.presence); } - if (res.session_start_limit && res.session_start_limit.remaining === 0) { - const { session_start_limit: { reset_after } } = res; - this.emit(Events.DEBUG, `Exceeded identify threshold, setting a timeout for ${reset_after} ms`); - await delayFor(reset_after); + + this.emit(Events.DEBUG, 'Preparing to connect to the gateway...'); + + try { + await this.ws.connect(); + return this.token; + } catch (error) { + this.destroy(); + throw error; } - const gateway = `${res.url}/`; - if (this.options.shardCount === 'auto') { - this.emit(Events.DEBUG, `Using recommended shard count ${res.shards}`); - this.options.shardCount = res.shards; - this.options.totalShardCount = res.shards; - if (typeof this.options.shards === 'undefined' || !this.options.shards.length) { - this.options.shards = []; - for (let i = 0; i < this.options.shardCount; ++i) this.options.shards.push(i); - } - } - this.emit(Events.DEBUG, `Using gateway ${gateway}`); - this.ws.connect(gateway); - await new Promise((resolve, reject) => { - const onready = () => { - clearTimeout(timeout); - this.removeListener(Events.DISCONNECT, ondisconnect); - resolve(); - }; - const ondisconnect = event => { - clearTimeout(timeout); - this.removeListener(Events.READY, onready); - this.destroy(); - if (WSCodes[event.code]) { - reject(new Error(WSCodes[event.code])); - } - }; - const timeout = setTimeout(() => { - this.removeListener(Events.READY, onready); - this.removeListener(Events.DISCONNECT, ondisconnect); - this.destroy(); - reject(new Error('WS_CONNECTION_TIMEOUT')); - }, this.options.shardCount * 25e3); - if (timeout.unref !== undefined) timeout.unref(); - this.once(Events.READY, onready); - this.once(Events.DISCONNECT, ondisconnect); - }); - return token; } /** @@ -397,9 +372,10 @@ class Client extends BaseClient { if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); } - if (options.shards && typeof options.shards !== 'number' && !Array.isArray(options.shards)) { + if (options.shards && !(options.shards instanceof Array)) { throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); } + if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS'); if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1'); if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index bc34b84b3..c483e803b 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,9 +1,10 @@ 'use strict'; +const { Error: DJSError } = require('../../errors'); const Collection = require('../../util/Collection'); const Util = require('../../util/Util'); const WebSocketShard = require('./WebSocketShard'); -const { Events, Status, WSEvents } = require('../../util/Constants'); +const { Events, ShardEvents, Status, WSCodes, WSEvents } = require('../../util/Constants'); const PacketHandlers = require('./handlers'); const BeforeReadyWhitelist = [ @@ -16,6 +17,8 @@ const BeforeReadyWhitelist = [ WSEvents.GUILD_MEMBER_REMOVE, ]; +const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; + /** * The WebSocket manager for this client. */ @@ -25,6 +28,7 @@ class WebSocketManager { * The client that instantiated this WebSocketManager * @type {Client} * @readonly + * @name WebSocketManager#client */ Object.defineProperty(this, 'client', { value: client }); @@ -34,6 +38,13 @@ class WebSocketManager { */ this.gateway = undefined; + /** + * The amount of shards this manager handles + * @private + * @type {number|string} + */ + this.totalShards = this.client.options.shardCount; + /** * A collection of all shards this manager handles * @type {Collection} @@ -41,18 +52,20 @@ class WebSocketManager { this.shards = new Collection(); /** - * An array of shards to be spawned or reconnected - * @type {Array} + * An array of shards to be connected or that need to reconnect + * @type {Set} * @private + * @name WebSocketManager#shardQueue */ - this.shardQueue = []; + Object.defineProperty(this, 'shardQueue', { value: new Set(), writable: true }); /** * An array of queued events before this WebSocketManager became ready * @type {object[]} * @private + * @name WebSocketManager#packetQueue */ - this.packetQueue = []; + Object.defineProperty(this, 'packetQueue', { value: [] }); /** * The current status of this WebSocketManager @@ -61,28 +74,28 @@ class WebSocketManager { this.status = Status.IDLE; /** - * If this manager is expected to close + * If this manager was destroyed. It will prevent shards from reconnecting * @type {boolean} * @private */ - this.expectingClose = false; + this.destroyed = false; + + /** + * If this manager is currently reconnecting one or multiple shards + * @type {boolean} + * @private + */ + this.reconnecting = false; /** * The current session limit of the client - * @type {?Object} * @private + * @type {?Object} * @prop {number} total Total number of identifies available * @prop {number} remaining Number of identifies remaining * @prop {number} reset_after Number of milliseconds after which the limit resets */ - this.sessionStartLimit = null; - - /** - * If the manager is currently reconnecting shards - * @type {boolean} - * @private - */ - this.isReconnectingShards = false; + this.sessionStartLimit = undefined; } /** @@ -96,121 +109,198 @@ class WebSocketManager { } /** - * Emits a debug event. - * @param {string} message Debug message + * Emits a debug message. + * @param {string} message The debug message + * @param {?WebSocketShard} [shard] The shard that emitted this message, if any * @private */ - debug(message) { - this.client.emit(Events.DEBUG, message); + debug(message, shard) { + this.client.emit(Events.DEBUG, `[WS => ${shard ? `Shard ${shard.id}` : 'Manager'}] ${message}`); } /** - * Checks if a new identify payload can be sent. + * Connects this manager to the gateway. * @private - * @returns {Promise} */ - async _checkSessionLimit() { - this.sessionStartLimit = await this.client.api.gateway.bot.get().then(r => r.session_start_limit); - const { remaining, reset_after } = this.sessionStartLimit; - if (remaining !== 0) return true; - return reset_after; - } + async connect() { + const invalidToken = new DJSError(WSCodes[4004]); + const { + url: gatewayURL, + shards: recommendedShards, + session_start_limit: sessionStartLimit, + } = await this.client.api.gateway.bot.get().catch(error => { + throw error.httpStatus === 401 ? invalidToken : error; + }); - /** - * Handles the session identify rate limit for creating a shard. - * @private - */ - async _handleSessionLimit() { - const canSpawn = await this._checkSessionLimit(); - if (typeof canSpawn === 'number') { - this.debug(`Exceeded identify threshold, setting a timeout for ${canSpawn}ms`); - await Util.delayFor(canSpawn); + this.sessionStartLimit = sessionStartLimit; + + const { total, remaining, reset_after } = sessionStartLimit; + + this.debug(`Fetched Gateway Information + URL: ${gatewayURL} + Recommended Shards: ${recommendedShards}`); + + this.debug(`Session Limit Information + Total: ${total} + Remaining: ${remaining}`); + + this.gateway = `${gatewayURL}/`; + + if (this.totalShards === 'auto') { + this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); + this.totalShards = this.client.options.shardCount = this.client.options.totalShardCount = recommendedShards; + if (typeof this.client.options.shards === 'undefined' || !this.client.options.shards.length) { + this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); + } } - this.create(); - } - /** - * Creates a connection to a gateway. - * @param {string} [gateway=this.gateway] The gateway to connect to - * @private - */ - connect(gateway = this.gateway) { - this.gateway = gateway; - - if (typeof this.client.options.shards === 'number') { - this.debug(`Spawning shard with ID ${this.client.options.shards}`); - this.shardQueue.push(this.client.options.shards); - } else if (Array.isArray(this.client.options.shards)) { - this.debug(`Spawning ${this.client.options.shards.length} shards`); - this.shardQueue.push(...this.client.options.shards); + if (this.client.options.shards instanceof Array) { + const { shards } = this.client.options; + this.totalShards = shards.length; + this.debug(`Spawning shards: ${shards.join(', ')}`); + this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); } else { - this.debug(`Spawning ${this.client.options.shardCount} shards`); - this.shardQueue.push(...Array.from({ length: this.client.options.shardCount }, (_, index) => index)); + this.debug(`Spawning ${this.totalShards} shards`); + this.shardQueue = new Set(Array.from({ length: this.totalShards }, (_, id) => new WebSocketShard(this, id))); } - this.create(); + + await this._handleSessionLimit(remaining, reset_after); + + return this.createShards(); } /** - * Creates or reconnects a shard. + * Handles the creation of a shard. + * @returns {Promise} * @private */ - create() { - // Nothing to create - if (!this.shardQueue.length) return; + async createShards() { + // If we don't have any shards to handle, return + if (!this.shardQueue.size) return false; - let item = this.shardQueue.shift(); - if (typeof item === 'string' && !isNaN(item)) item = Number(item); + const [shard] = this.shardQueue; - if (item instanceof WebSocketShard) { - const timeout = setTimeout(() => { - this.debug(`[Shard ${item.id}] Failed to connect in 15s... Destroying and trying again`); - item.destroy(); - if (!this.shardQueue.includes(item)) this.shardQueue.push(item); - this.reconnect(true); - }, 15000); - item.once(Events.READY, this._shardReady.bind(this, timeout)); - item.once(Events.RESUMED, this._shardReady.bind(this, timeout)); - item.connect(); - return; + this.shardQueue.delete(shard); + + if (!shard.eventsAttached) { + shard.on(ShardEvents.READY, () => { + /** + * Emitted when a shard turns ready. + * @event Client#shardReady + * @param {number} id The shard ID that turned ready + */ + this.client.emit(Events.SHARD_READY, shard.id); + + if (!this.shardQueue.size) this.reconnecting = false; + }); + + shard.on(ShardEvents.RESUMED, () => { + /** + * Emitted when a shard resumes successfully. + * @event Client#shardResumed + * @param {number} id The shard ID that resumed + */ + this.client.emit(Events.SHARD_RESUMED, shard.id); + }); + + shard.on(ShardEvents.CLOSE, event => { + if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { + /** + * Emitted when a shard's WebSocket disconnects and will no longer reconnect. + * @event Client#shardDisconnected + * @param {CloseEvent} event The WebSocket close event + * @param {number} id The shard ID that disconnected + */ + this.client.emit(Events.SHARD_DISCONNECTED, event, shard.id); + this.debug(WSCodes[event.code], shard); + return; + } + + if (event.code >= 1000 && event.code <= 2000) { + // Any event code in this range cannot be resumed. + shard.sessionID = undefined; + } + + /** + * Emitted when a shard is attempting to reconnect or re-identify. + * @event Client#shardReconnecting + * @param {number} id The shard ID that is attempting to reconnect + */ + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + if (shard.sessionID) { + this.debug(`Session ID is present, attempting an immediate reconnect...`, shard); + shard.connect().catch(() => null); + return; + } + + shard.destroy(); + + this.shardQueue.add(shard); + this.reconnect(); + }); + + shard.on(ShardEvents.INVALID_SESSION, () => { + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + this.shardQueue.add(shard); + this.reconnect(); + }); + + shard.on(ShardEvents.DESTROYED, () => { + this.debug('Shard was destroyed but no WebSocket connection existed... Reconnecting...', shard); + + this.client.emit(Events.SHARD_RECONNECTING, shard.id); + + this.shardQueue.add(shard); + this.reconnect(); + }); + + shard.eventsAttached = true; } - const shard = new WebSocketShard(this, item); - this.shards.set(item, shard); - shard.once(Events.READY, this._shardReady.bind(this)); + this.shards.set(shard.id, shard); + + try { + await shard.connect(); + } catch (error) { + if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { + throw new DJSError(WSCodes[error.code]); + } else { + this.debug('Failed to connect to the gateway, requeueing...', shard); + this.shardQueue.add(shard); + } + } + // If we have more shards, add a 5s delay + if (this.shardQueue.size) { + this.debug(`Shard Queue Size: ${this.shardQueue.size}; continuing in 5 seconds...`); + await Util.delayFor(5000); + await this._handleSessionLimit(); + return this.createShards(); + } + + return true; } /** - * Shared handler for shards turning ready or resuming. - * @param {Timeout} [timeout=null] Optional timeout to clear if shard didn't turn ready in time + * Handles reconnects for this manager. * @private + * @returns {Promise} */ - _shardReady(timeout = null) { - if (timeout) clearTimeout(timeout); - if (this.shardQueue.length) { - this.client.setTimeout(this._handleSessionLimit.bind(this), 5000); - } else { - this.isReconnectingShards = false; - } - } - - /** - * Handles the reconnect of a shard. - * @param {WebSocketShard|boolean} shard The shard to reconnect, or a boolean to indicate an immediate reconnect - * @private - */ - async reconnect(shard) { - // If the item is a shard, add it to the queue - if (shard instanceof WebSocketShard) this.shardQueue.push(shard); - if (typeof shard === 'boolean') { - // If a boolean is passed, force a reconnect right now - } else if (this.isReconnectingShards) { - // If we're already reconnecting shards, and no boolean was provided, return - return; - } - this.isReconnectingShards = true; + async reconnect() { + if (this.reconnecting || this.status !== Status.READY) return false; + this.reconnecting = true; try { await this._handleSessionLimit(); + await this.createShards(); } catch (error) { + this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); + if (error.httpStatus !== 401) { + this.debug(`Possible network error occured. Retrying in 5s...`); + await Util.delayFor(5000); + this.reconnecting = false; + return this.reconnect(); + } // If we get an error at this point, it means we cannot reconnect anymore if (this.client.listenerCount(Events.INVALIDATED)) { /** @@ -225,6 +315,52 @@ class WebSocketManager { } else { this.client.destroy(); } + } finally { + this.reconnecting = false; + } + return true; + } + + /** + * Broadcasts a packet to every shard this manager handles. + * @param {Object} packet The packet to send + * @private + */ + broadcast(packet) { + for (const shard of this.shards.values()) shard.send(packet); + } + + /** + * Destroys this manager and all its shards. + * @private + */ + destroy() { + if (this.destroyed) return; + this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); + this.destroyed = true; + this.shardQueue.clear(); + for (const shard of this.shards.values()) shard.destroy(); + } + + /** + * Handles the timeout required if we cannot identify anymore. + * @param {number} [remaining] The amount of remaining identify sessions that can be done today + * @param {number} [resetAfter] The amount of time in which the identify counter resets + * @private + */ + async _handleSessionLimit(remaining, resetAfter) { + if (typeof remaining === 'undefined' && typeof resetAfter === 'undefined') { + const { session_start_limit } = await this.client.api.gateway.bot.get(); + this.sessionStartLimit = session_start_limit; + remaining = session_start_limit.remaining; + resetAfter = session_start_limit.reset_after; + this.debug(`Session Limit Information + Total: ${session_start_limit.total} + Remaining: ${remaining}`); + } + if (!remaining) { + this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`); + await Util.delayFor(resetAfter); } } @@ -263,15 +399,13 @@ class WebSocketManager { * @private */ checkReady() { - if (this.shards.size !== this.client.options.shardCount || - this.shards.some(s => s.status !== Status.READY)) { + if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) { return false; } - let unavailableGuilds = 0; - for (const guild of this.client.guilds.values()) { - if (!guild.available) unavailableGuilds++; - } + const unavailableGuilds = this.client.guilds.reduce((acc, guild) => guild.available ? acc : acc + 1, 0); + + // TODO: Rethink implementation for this if (unavailableGuilds === 0) { this.status = Status.NEARLY; if (!this.client.options.fetchAllMembers) return this.triggerReady(); @@ -280,16 +414,18 @@ class WebSocketManager { Promise.all(promises) .then(() => this.triggerReady()) .catch(e => { - this.debug(`Failed to fetch all members before ready! ${e}`); + this.debug(`Failed to fetch all members before ready! ${e}\n${e.stack}`); this.triggerReady(); }); + } else { + this.debug(`There are ${unavailableGuilds} unavailable guilds. Waiting for their GUILD_CREATE packets`); } + return true; } /** * Causes the client to be marked as ready and emits the ready event. - * @returns {void} * @private */ triggerReady() { @@ -303,31 +439,10 @@ class WebSocketManager { * Emitted when the client becomes ready to start working. * @event Client#ready */ - this.client.emit(Events.READY); + this.client.emit(Events.CLIENT_READY); this.handlePacket(); } - - /** - * Broadcasts a message to every shard in this WebSocketManager. - * @param {*} packet The packet to send - * @private - */ - broadcast(packet) { - for (const shard of this.shards.values()) shard.send(packet); - } - - /** - * Destroys all shards. - * @private - */ - destroy() { - if (this.expectingClose) return; - this.expectingClose = true; - this.isReconnectingShards = false; - this.shardQueue.length = 0; - for (const shard of this.shards.values()) shard.destroy(); - } } module.exports = WebSocketManager; diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 96ab06a44..19cb11e4b 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -2,8 +2,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); -const { Status, Events, OPCodes, WSEvents, WSCodes } = require('../../util/Constants'); -const Util = require('../../util/Util'); +const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); let zlib; try { @@ -21,13 +20,13 @@ class WebSocketShard extends EventEmitter { super(); /** - * The WebSocket Manager of this connection + * The WebSocketManager of the shard * @type {WebSocketManager} */ this.manager = manager; /** - * The ID of the this shard + * The ID of the shard * @type {number} */ this.id = id; @@ -91,20 +90,22 @@ class WebSocketShard extends EventEmitter { * @type {Object} * @private */ - this.ratelimit = { - queue: [], - total: 120, - remaining: 120, - time: 60e3, - timer: null, - }; + Object.defineProperty(this, 'ratelimit', { + value: { + queue: [], + total: 120, + remaining: 120, + time: 60e3, + timer: null, + }, + }); /** * The WebSocket connection for the current shard * @type {?WebSocket} * @private */ - this.connection = null; + Object.defineProperty(this, 'connection', { value: null, writable: true }); /** * @external Inflate @@ -116,9 +117,21 @@ class WebSocketShard extends EventEmitter { * @type {?Inflate} * @private */ - this.inflate = null; + Object.defineProperty(this, 'inflate', { value: null, writable: true }); - if (this.manager.gateway) this.connect(); + /** + * The HELLO timeout + * @type {?NodeJS.Timer} + * @private + */ + Object.defineProperty(this, 'helloTimeout', { value: null, writable: true }); + + /** + * If the manager attached its event handlers on the shard + * @type {boolean} + * @private + */ + Object.defineProperty(this, 'eventsAttached', { value: false, writable: true }); } /** @@ -133,82 +146,86 @@ class WebSocketShard extends EventEmitter { /** * Emits a debug event. - * @param {string} message Debug message + * @param {string} message The debug message * @private */ debug(message) { - this.manager.debug(`[Shard ${this.id}] ${message}`); + this.manager.debug(message, this); } /** - * Sends a heartbeat to the WebSocket. - * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect - * @private - */ - sendHeartbeat() { - if (!this.lastHeartbeatAcked) { - this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); - this.connection.close(4000); - return; - } - this.debug('Sending a heartbeat'); - this.lastHeartbeatAcked = false; - this.lastPingTimestamp = Date.now(); - this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }); - } - - /** - * Sets the heartbeat timer for this shard. - * @param {number} time If -1, clears the interval, any other number sets an interval - * @private - */ - setHeartbeatTimer(time) { - if (time === -1) { - if (this.heartbeatInterval) { - this.debug('Clearing heartbeat interval'); - this.manager.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } - return; - } - this.debug(`Setting a heartbeat interval for ${time}ms`); - this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); - } - - /** - * Acknowledges a heartbeat. - * @private - */ - ackHeartbeat() { - this.lastHeartbeatAcked = true; - const latency = Date.now() - this.lastPingTimestamp; - this.debug(`Heartbeat acknowledged, latency of ${latency}ms`); - this.pings.unshift(latency); - if (this.pings.length > 3) this.pings.length = 3; - } - - /** - * Connects this shard to the gateway. + * Connects the shard to the gateway. * @private + * @returns {Promise} A promise that will resolve if the shard turns ready successfully, + * or reject if we couldn't connect */ connect() { - const { expectingClose, gateway } = this.manager; - if (expectingClose) return; - this.inflate = new zlib.Inflate({ - chunkSize: 65535, - flush: zlib.Z_SYNC_FLUSH, - to: WebSocket.encoding === 'json' ? 'string' : '', + const { gateway, client } = this.manager; + + if (this.status === Status.READY && this.connection && this.connection.readyState === WebSocket.OPEN) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const onReady = () => { + this.off(ShardEvents.CLOSE, onClose); + this.off(ShardEvents.RESUMED, onResumed); + this.off(ShardEvents.INVALID_SESSION, onInvalid); + resolve(); + }; + + const onResumed = () => { + this.off(ShardEvents.CLOSE, onClose); + this.off(ShardEvents.READY, onReady); + this.off(ShardEvents.INVALID_SESSION, onInvalid); + resolve(); + }; + + const onClose = event => { + this.off(ShardEvents.READY, onReady); + this.off(ShardEvents.RESUMED, onResumed); + this.off(ShardEvents.INVALID_SESSION, onInvalid); + reject(event); + }; + + const onInvalid = () => { + this.off(ShardEvents.READY, onReady); + this.off(ShardEvents.RESUMED, onResumed); + this.off(ShardEvents.CLOSE, onClose); + // eslint-disable-next-line prefer-promise-reject-errors + reject(); + }; + + this.once(ShardEvents.READY, onReady); + this.once(ShardEvents.RESUMED, onResumed); + this.once(ShardEvents.CLOSE, onClose); + this.once(ShardEvents.INVALID_SESSION, onInvalid); + + if (this.connection && this.connection.readyState === WebSocket.OPEN) { + this.identifyNew(); + return; + } + + this.inflate = new zlib.Inflate({ + chunkSize: 65535, + flush: zlib.Z_SYNC_FLUSH, + to: WebSocket.encoding === 'json' ? 'string' : '', + }); + + this.debug(`Trying to connect to ${gateway}, version ${client.options.ws.version}`); + + this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; + this.setHelloTimeout(); + + const ws = this.connection = WebSocket.create(gateway, { + v: client.options.ws.version, + compress: 'zlib-stream', + }); + ws.onopen = this.onOpen.bind(this); + ws.onmessage = this.onMessage.bind(this); + ws.onerror = this.onError.bind(this); + ws.onclose = this.onClose.bind(this); }); - this.debug(`Connecting to ${gateway}`); - const ws = this.connection = WebSocket.create(gateway, { - v: this.manager.client.options.ws.version, - compress: 'zlib-stream', - }); - ws.onopen = this.onOpen.bind(this); - ws.onmessage = this.onMessage.bind(this); - ws.onerror = this.onError.bind(this); - ws.onclose = this.onClose.bind(this); - this.status = Status.CONNECTING; } /** @@ -216,7 +233,8 @@ class WebSocketShard extends EventEmitter { * @private */ onOpen() { - this.debug('Connected to the gateway'); + this.debug('Opened a connection to the gateway successfully.'); + this.status = Status.NEARLY; } /** @@ -240,53 +258,106 @@ class WebSocketShard extends EventEmitter { packet = WebSocket.unpack(this.inflate.result); this.manager.client.emit(Events.RAW, packet, this.id); } catch (err) { - this.manager.client.emit(Events.ERROR, err); + this.manager.client.emit(Events.SHARD_ERROR, err, this.id); return; } this.onPacket(packet); } + /** + * Called whenever an error occurs with the WebSocket. + * @param {ErrorEvent} error The error that occurred + * @private + */ + onError({ error }) { + if (error && error.message === 'uWs client connection error') { + this.debug('Received a uWs error. Closing the connection and reconnecting...'); + this.connection.close(4000); + return; + } + + /** + * Emitted whenever a shard's WebSocket encounters a connection error. + * @event Client#shardError + * @param {Error} error The encountered error + * @param {number} shardID The shard that encountered this error + */ + this.manager.client.emit(Events.SHARD_ERROR, error, this.id); + } + + /** + * @external CloseEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} + */ + + /** + * @external ErrorEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent} + */ + + /** + * Called whenever a connection to the gateway is closed. + * @param {CloseEvent} event Close event that was received + * @private + */ + onClose(event) { + this.closeSequence = this.sequence; + this.sequence = -1; + this.debug(`WebSocket was closed. + Event Code: ${event.code} + Clean: ${event.wasClean} + Reason: ${event.reason || 'No reason received'}`); + + this.status = Status.DISCONNECTED; + + /** + * Emitted when a shard's WebSocket closes. + * @private + * @event WebSocketShard#close + * @param {CloseEvent} event The received event + */ + this.emit(ShardEvents.CLOSE, event); + } + /** * Called whenever a packet is received. - * @param {Object} packet Packet received + * @param {Object} packet The received packet * @private */ onPacket(packet) { if (!packet) { - this.debug('Received null or broken packet'); + this.debug(`Received broken packet: ${packet}.`); return; } switch (packet.t) { case WSEvents.READY: /** - * Emitted when a shard becomes ready. + * Emitted when the shard becomes ready * @event WebSocketShard#ready */ - this.emit(Events.READY); - /** - * Emitted when a shard becomes ready. - * @event Client#shardReady - * @param {number} shardID The ID of the shard - */ - this.manager.client.emit(Events.SHARD_READY, this.id); + this.emit(ShardEvents.READY); this.sessionID = packet.d.session_id; this.trace = packet.d._trace; this.status = Status.READY; - this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}`); + this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); break; case WSEvents.RESUMED: { - this.emit(Events.RESUMED); + /** + * Emitted when the shard resumes successfully + * @event WebSocketShard#resumed + */ + this.emit(ShardEvents.RESUMED); + this.trace = packet.d._trace; this.status = Status.READY; const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED ${this.trace.join(' -> ')} | Replayed ${replayed} events.`); + this.debug(`RESUMED ${this.trace.join(' -> ')} | Session ${this.sessionID} | Replayed ${replayed} events.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); - break; } } @@ -294,6 +365,7 @@ class WebSocketShard extends EventEmitter { switch (packet.op) { case OPCodes.HELLO: + this.setHelloTimeout(-1); this.setHeartbeatTimer(packet.d.heartbeat_interval); this.identify(); break; @@ -301,21 +373,20 @@ class WebSocketShard extends EventEmitter { this.connection.close(1001); break; case OPCodes.INVALID_SESSION: - this.debug(`Session was invalidated. Resumable: ${packet.d}.`); - // If the session isn't resumable - if (!packet.d) { - // Reset the sequence, since it isn't valid anymore - this.sequence = -1; - // If we had a session ID before - if (this.sessionID) { - this.sessionID = null; - this.connection.close(1000); - return; - } - this.connection.close(1000); + this.debug(`Session invalidated. Resumable: ${packet.d}.`); + // If we can resume the session, do so immediately + if (packet.d) { + this.identifyResume(); return; } - this.identifyResume(); + // Reset the sequence + this.sequence = -1; + // Reset the session ID as it's invalid + this.sessionID = null; + // Set the status to reconnecting + this.status = Status.RECONNECTING; + // Finally, emit the INVALID_SESSION event + this.emit(ShardEvents.INVALID_SESSION); break; case OPCodes.HEARTBEAT_ACK: this.ackHeartbeat(); @@ -329,10 +400,78 @@ class WebSocketShard extends EventEmitter { } /** - * Identifies the client on a connection. - * @returns {void} + * Sets the HELLO packet timeout. + * @param {number} [time] If set to -1, it will clear the hello timeout timeout * @private */ + setHelloTimeout(time) { + if (time === -1) { + if (this.helloTimeout) { + this.debug('Clearing the HELLO timeout.'); + this.manager.client.clearTimeout(this.helloTimeout); + this.helloTimeout = null; + } + return; + } + this.debug('Setting a HELLO timeout for 20s.'); + this.helloTimeout = this.manager.client.setTimeout(() => { + this.debug('Did not receive HELLO in time. Destroying and connecting again.'); + this.destroy(4009); + }, 20000); + } + + /** + * Sets the heartbeat timer for this shard. + * @param {number} time If -1, clears the interval, any other number sets an interval + * @private + */ + setHeartbeatTimer(time) { + if (time === -1) { + if (this.heartbeatInterval) { + this.debug('Clearing the heartbeat interval.'); + this.manager.client.clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + return; + } + this.debug(`Setting a heartbeat interval for ${time}ms.`); + this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); + } + + /** + * Sends a heartbeat to the WebSocket. + * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect + * @private + */ + sendHeartbeat() { + if (!this.lastHeartbeatAcked) { + this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); + this.destroy(4009); + return; + } + this.debug('Sending a heartbeat.'); + this.lastHeartbeatAcked = false; + this.lastPingTimestamp = Date.now(); + this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }, true); + } + + /** + * Acknowledges a heartbeat. + * @private + */ + ackHeartbeat() { + this.lastHeartbeatAcked = true; + const latency = Date.now() - this.lastPingTimestamp; + this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`); + this.pings.unshift(latency); + if (this.pings.length > 3) this.pings.length = 3; + } + + /** + * Identifies the client on the connection. + * @private + * @returns {void} + */ identify() { return this.sessionID ? this.identifyResume() : this.identifyNew(); } @@ -342,31 +481,32 @@ class WebSocketShard extends EventEmitter { * @private */ identifyNew() { - if (!this.manager.client.token) { - this.debug('No token available to identify a new session with'); + const { client } = this.manager; + if (!client.token) { + this.debug('No token available to identify a new session.'); return; } - // Clone the generic payload and assign the token + + // Clone the identify payload and assign the token and shard info const d = { - ...this.manager.client.options.ws, - token: this.manager.client.token, - shard: [this.id, Number(this.manager.client.options.totalShardCount)], + ...client.options.ws, + token: client.token, + shard: [this.id, Number(client.options.totalShardCount)], }; - // Send the payload - this.debug('Identifying as a new session'); - this.send({ op: OPCodes.IDENTIFY, d }); + this.debug(`Identifying as a new session. Shard ${this.id}/${client.options.totalShardCount}`); + this.send({ op: OPCodes.IDENTIFY, d }, true); } /** * Resumes a session on the gateway. - * @returns {void} * @private */ identifyResume() { if (!this.sessionID) { - this.debug('Warning: wanted to resume but session ID not available; identifying as a new session instead'); - return this.identifyNew(); + this.debug('Warning: attempted to resume but no session ID was present; identifying as a new session.'); + this.identifyNew(); + return; } this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`); @@ -377,85 +517,19 @@ class WebSocketShard extends EventEmitter { seq: this.closeSequence, }; - return this.send({ op: OPCodes.RESUME, d }); + this.send({ op: OPCodes.RESUME, d }, true); } /** - * Called whenever an error occurs with the WebSocket. - * @param {Error} error The error that occurred - * @private + * Adds a packet to the queue to be sent to the gateway. + * If you use this method, make sure you understand that you need to provide + * a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands). + * Do not use this method if you don't know what you're doing. + * @param {Object} data The full packet to send + * @param {?boolean} [important=false] If this packet should be added first in queue */ - onError(error) { - if (error && error.message === 'uWs client connection error') { - this.connection.close(4000); - return; - } - - /** - * Emitted whenever the client's WebSocket encounters a connection error. - * @event Client#error - * @param {Error} error The encountered error - * @param {number} shardID The shard that encountered this error - */ - this.manager.client.emit(Events.ERROR, error, this.id); - } - - /** - * @external CloseEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} - */ - - /** - * Called whenever a connection to the gateway is closed. - * @param {CloseEvent} event Close event that was received - * @private - */ - onClose(event) { - this.closeSequence = this.sequence; - this.debug(`WebSocket was closed. - Event Code: ${event.code} - Reason: ${event.reason}`); - - if (event.code === 1000 ? this.manager.expectingClose : WSCodes[event.code]) { - /** - * Emitted when the client's WebSocket disconnects and will no longer attempt to reconnect. - * @event Client#disconnect - * @param {CloseEvent} event The WebSocket close event - * @param {number} shardID The shard that disconnected - */ - this.manager.client.emit(Events.DISCONNECT, event, this.id); - this.debug(WSCodes[event.code]); - return; - } - - this.destroy(); - - this.status = Status.RECONNECTING; - - /** - * Emitted whenever a shard tries to reconnect to the WebSocket. - * @event Client#reconnecting - * @param {number} shardID The shard ID that is reconnecting - */ - this.manager.client.emit(Events.RECONNECTING, this.id); - - this.debug(`${this.sessionID ? `Reconnecting in 3500ms` : 'Queueing a reconnect'} to the gateway...`); - - if (this.sessionID) { - Util.delayFor(3500).then(() => this.connect()); - } else { - this.manager.reconnect(this); - } - } - - /** - * Adds data to the queue to be sent. - * @param {Object} data Packet to send - * @private - * @returns {void} - */ - send(data) { - this.ratelimit.queue.push(data); + send(data, important = false) { + this.ratelimit.queue[important ? 'unshift' : 'push'](data); this.processQueue(); } @@ -472,7 +546,7 @@ class WebSocketShard extends EventEmitter { } this.connection.send(WebSocket.pack(data), err => { - if (err) this.manager.client.emit(Events.ERROR, err); + if (err) this.manager.client.emit(Events.SHARD_ERROR, err, this.id); }); } @@ -499,21 +573,36 @@ class WebSocketShard extends EventEmitter { } /** - * Destroys this shard and closes its connection. + * Destroys this shard and closes its WebSocket connection. + * @param {?number} [closeCode=1000] The close code to use * @private */ - destroy() { + destroy(closeCode = 1000) { this.setHeartbeatTimer(-1); - if (this.connection) this.connection.close(1000); + this.setHelloTimeout(-1); + // Close the WebSocket connection, if any + if (this.connection) { + this.connection.close(closeCode); + } else { + /** + * Emitted when a shard is destroyed, but no WebSocket connection was present. + * @private + * @event WebSocketShard#destroyed + */ + this.emit(ShardEvents.DESTROYED); + } this.connection = null; + // Set the shard status this.status = Status.DISCONNECTED; + // Reset the sequence + this.sequence = -1; + // Reset the ratelimit data this.ratelimit.remaining = this.ratelimit.total; this.ratelimit.queue.length = 0; if (this.ratelimit.timer) { this.manager.client.clearTimeout(this.ratelimit.timer); this.ratelimit.timer = null; } - this.sequence = -1; } } diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 5945c167f..1cfe4da9f 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -4,12 +4,12 @@ const { register } = require('./DJSError'); const Messages = { CLIENT_INVALID_OPTION: (prop, must) => `The ${prop} option must be ${must}`, + CLIENT_INVALID_PROVIDED_SHARDS: 'None of the provided shards were valid.', TOKEN_INVALID: 'An invalid token was provided.', TOKEN_MISSING: 'Request to use token, but token was unavailable to the client.', WS_CLOSE_REQUESTED: 'WebSocket closed due to user request.', - WS_CONNECTION_TIMEOUT: 'The connection to the gateway timed out.', WS_CONNECTION_EXISTS: 'There is already an existing WebSocket connection.', WS_NOT_OPEN: (data = 'data') => `Websocket not open to send ${data}`, diff --git a/src/rest/DiscordAPIError.js b/src/rest/DiscordAPIError.js index 559194f2c..3ccd2b33f 100644 --- a/src/rest/DiscordAPIError.js +++ b/src/rest/DiscordAPIError.js @@ -5,7 +5,7 @@ * @extends Error */ class DiscordAPIError extends Error { - constructor(path, error, method) { + constructor(path, error, method, status) { super(); const flattened = this.constructor.flattenErrors(error.errors || error).join('\n'); this.name = 'DiscordAPIError'; @@ -28,6 +28,12 @@ class DiscordAPIError extends Error { * @type {number} */ this.code = error.code; + + /** + * The HTTP status code + * @type {number} + */ + this.httpStatus = status; } /** diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 44fca4fbc..45065a662 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -167,7 +167,7 @@ class RequestHandler { try { const data = await parseResponse(res); if (res.status >= 400 && res.status < 500) { - return reject(new DiscordAPIError(request.path, data, request.method)); + return reject(new DiscordAPIError(request.path, data, request.method, res.status)); } return null; } catch (err) { diff --git a/src/util/Constants.js b/src/util/Constants.js index b6c1b0355..dfd370f71 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -212,8 +212,7 @@ exports.VoiceOPCodes = { exports.Events = { RATE_LIMIT: 'rateLimit', - READY: 'ready', - RESUMED: 'resumed', + CLIENT_READY: 'ready', GUILD_CREATE: 'guildCreate', GUILD_DELETE: 'guildDelete', GUILD_UPDATE: 'guildUpdate', @@ -246,8 +245,6 @@ exports.Events = { MESSAGE_REACTION_REMOVE: 'messageReactionRemove', MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', USER_UPDATE: 'userUpdate', - USER_NOTE_UPDATE: 'userNoteUpdate', - USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_SERVER_UPDATE: 'voiceServerUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', @@ -256,16 +253,26 @@ exports.Events = { TYPING_START: 'typingStart', TYPING_STOP: 'typingStop', WEBHOOKS_UPDATE: 'webhookUpdate', - DISCONNECT: 'disconnect', - RECONNECTING: 'reconnecting', ERROR: 'error', WARN: 'warn', DEBUG: 'debug', + SHARD_DISCONNECTED: 'shardDisconnected', + SHARD_ERROR: 'shardError', + SHARD_RECONNECTING: 'shardReconnecting', SHARD_READY: 'shardReady', + SHARD_RESUMED: 'shardResumed', INVALIDATED: 'invalidated', RAW: 'raw', }; +exports.ShardEvents = { + CLOSE: 'close', + DESTROYED: 'destroyed', + INVALID_SESSION: 'invalidSession', + READY: 'ready', + RESUMED: 'resumed', +}; + /** * The type of Structure allowed to be a partial: * * USER @@ -312,7 +319,6 @@ exports.PartialTypes = keyMirror([ * * MESSAGE_REACTION_REMOVE * * MESSAGE_REACTION_REMOVE_ALL * * USER_UPDATE - * * USER_NOTE_UPDATE * * USER_SETTINGS_UPDATE * * PRESENCE_UPDATE * * VOICE_STATE_UPDATE diff --git a/typings/index.d.ts b/typings/index.d.ts index f77ab51ee..3c3e118f3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2,6 +2,7 @@ declare module 'discord.js' { import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; + import * as WebSocket from 'ws'; export const version: string; @@ -181,15 +182,18 @@ declare module 'discord.js' { public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; - public on(event: 'reconnecting', listener: (shardID: number) => void): this; - public on(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public on(event: 'shardReady', listener: (shardID: number) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; + public on(event: 'invalidated', listener: () => void): this; + public on(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public on(event: 'shardError', listener: (error: Error, id: number) => void): this; + public on(event: 'shardReconnecting', listener: (id: number) => void): this; + public on(event: 'shardReady', listener: (id: number) => void): this; + public on(event: 'shardResumed', listener: (id: number) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -215,15 +219,18 @@ declare module 'discord.js' { public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; - public once(event: 'reconnecting', listener: (shardID: number) => void): this; - public once(event: 'resumed', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public once(event: 'shardReady', listener: (shardID: number) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; + public once(event: 'invalidated', listener: () => void): this; + public once(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public once(event: 'shardError', listener: (error: Error, id: number) => void): this; + public once(event: 'shardReconnecting', listener: (id: number) => void): this; + public once(event: 'shardReady', listener: (id: number) => void): this; + public once(event: 'shardResumed', listener: (id: number) => void): this; public once(event: string, listener: Function): this; } @@ -340,12 +347,13 @@ declare module 'discord.js' { } export class DiscordAPIError extends Error { - constructor(path: string, error: object, method: string); + constructor(path: string, error: object, method: string, httpStatus: number); private static flattenErrors(obj: object, key: string): string[]; public code: number; public method: string; public path: string; + public httpStatus: number; } export class DMChannel extends TextBasedChannel(Channel) { @@ -1270,27 +1278,80 @@ declare module 'discord.js' { export class WebSocketManager { constructor(client: Client); + private totalShards: number | string; + private shardQueue: Set; + private packetQueue: object[]; + private destroyed: boolean; + private reconnecting: boolean; + private sessionStartLimit?: { total: number; remaining: number; reset_after: number; }; + public readonly client: Client; - public gateway: string | undefined; - public readonly ping: number; + public gateway?: string; public shards: Collection; public status: Status; + public readonly ping: number; - public broadcast(packet: object): void; + private debug(message: string, shard?: WebSocketShard): void; + private connect(): Promise; + private createShards(): Promise; + private reconnect(): Promise; + private broadcast(packet: object): void; + private destroy(): void; + private _handleSessionLimit(remaining?: number, resetAfter?: number): Promise; + private handlePacket(packet?: object, shard?: WebSocketShard): Promise; + private checkReady(): boolean; + private triggerReady(): void; } export class WebSocketShard extends EventEmitter { constructor(manager: WebSocketManager, id: number); - public id: number; - public readonly ping: number; - public pings: number[]; - public status: Status; + private sequence: number; + private closeSequence: number; + private sessionID?: string; + private lastPingTimestamp: number; + private lastHeartbeatAcked: boolean; + private trace: string[]; + private ratelimit: { queue: object[]; total: number; remaining: number; time: 60e3; timer: NodeJS.Timeout | null; }; + private connection: WebSocket | null; + private helloTimeout: NodeJS.Timeout | null; + private eventsAttached: boolean; + public manager: WebSocketManager; + public id: number; + public status: Status; + public pings: [number, number, number]; + public readonly ping: number; - public send(packet: object): void; + private debug(message: string): void; + private connect(): Promise; + private onOpen(): void; + private onMessage(event: MessageEvent): void; + private onError(error: ErrorEvent): void; + private onClose(event: CloseEvent): void; + private onPacket(packet: object): void; + private setHelloTimeout(time?: number): void; + private setHeartbeatTimer(time: number): void; + private sendHeartbeat(): void; + private ackHeartbeat(): void; + private identify(): void; + private identifyNew(): void; + private identifyResume(): void; + private _send(data: object): void; + private processQueue(): void; + private destroy(closeCode: number): void; + public send(data: object): void; public on(event: 'ready', listener: () => void): this; + public on(event: 'resumed', listener: () => void): this; + public on(event: 'close', listener: (event: CloseEvent) => void): this; + public on(event: 'invalidSession', listener: () => void): this; + public on(event: string, listener: Function): this; + public once(event: 'ready', listener: () => void): this; + public once(event: 'resumed', listener: () => void): this; + public once(event: 'close', listener: (event: CloseEvent) => void): this; + public once(event: 'invalidSession', listener: () => void): this; + public once(event: string, listener: Function): this; } //#endregion @@ -1589,7 +1650,7 @@ declare module 'discord.js' { interface ClientOptions { shards?: number | number[]; - shardCount?: number; + shardCount?: number | 'auto'; totalShardCount?: number; messageCacheMaxSize?: number; messageCacheLifetime?: number; @@ -2149,5 +2210,9 @@ declare module 'discord.js' { | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; + type MessageEvent = { data: WebSocket.Data; type: string; target: WebSocket; }; + type CloseEvent = { wasClean: boolean; code: number; reason: string; target: WebSocket; }; + type ErrorEvent = { error: any, message: string, type: string, target: WebSocket; }; + //#endregion } From 32a432f4a55094ea7c0f6260529f8f911ea8a2de Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 1 Apr 2019 13:46:27 +0200 Subject: [PATCH 1004/1359] cleanup(ShardClientUtil): rename id to ids --- src/sharding/ShardClientUtil.js | 6 +++--- typings/index.d.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 026c97efa..acbe22508 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -46,11 +46,11 @@ class ShardClientUtil { } /** - * Shard ID or array of shard IDs of this client - * @type {number|number[]} + * Array of shard IDs of this client + * @type {number[]} * @readonly */ - get id() { + get ids() { return this.client.options.shards; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 3c3e118f3..d0383f694 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -937,7 +937,7 @@ declare module 'discord.js' { public client: Client; public readonly count: number; - public readonly id: number | number[]; + public readonly ids: number[]; public mode: ShardingManagerMode; public parentPort: any; public broadcastEval(script: string): Promise; From 089f65fd2acb284fa741f4faf302ec34660026b6 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 3 Apr 2019 22:45:51 +0200 Subject: [PATCH 1005/1359] fix(RequestHandler): pass HTTPError pass path instead of route as path --- src/rest/RequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 45065a662..4e10b5feb 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -103,7 +103,7 @@ class RequestHandler { // NodeFetch error expected for all "operational" errors, such as 500 status code this.busy = false; return reject( - new HTTPError(error.message, error.constructor.name, error.status, request.method, request.route) + new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path) ); } From bb92289e45b4d9d576a4277d0bfa082814d2bc20 Mon Sep 17 00:00:00 2001 From: bdistin Date: Wed, 3 Apr 2019 16:02:19 -0500 Subject: [PATCH 1006/1359] fix: remove GuildChannel fallback, and remove GuildChannel as extendable (#3165) * remake pr * typings --- src/stores/ChannelStore.js | 2 +- src/structures/Channel.js | 6 +----- src/util/Structures.js | 1 - typings/index.d.ts | 1 - 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 88c5a0bb3..aad7927aa 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -60,7 +60,7 @@ class ChannelStore extends DataStore { const channel = Channel.create(this.client, data, guild); if (!channel) { - this.client.emit(Events.DEBUG, `Failed to find guild for channel ${data.id} ${data.type}`); + this.client.emit(Events.DEBUG, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`); return null; } diff --git a/src/structures/Channel.js b/src/structures/Channel.js index b43411949..de5f462e8 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -116,12 +116,8 @@ class Channel extends Base { channel = new CategoryChannel(guild, data); break; } - default: { - const GuildChannel = Structures.get('GuildChannel'); - channel = new GuildChannel(guild, data); - } } - guild.channels.set(channel.id, channel); + if (channel) guild.channels.set(channel.id, channel); } } return channel; diff --git a/src/util/Structures.js b/src/util/Structures.js index 62646b404..5cdfc831a 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -75,7 +75,6 @@ const structures = { TextChannel: require('../structures/TextChannel'), VoiceChannel: require('../structures/VoiceChannel'), CategoryChannel: require('../structures/CategoryChannel'), - GuildChannel: require('../structures/GuildChannel'), GuildMember: require('../structures/GuildMember'), Guild: require('../structures/Guild'), Message: require('../structures/Message'), diff --git a/typings/index.d.ts b/typings/index.d.ts index d0383f694..f1547c876 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1735,7 +1735,6 @@ declare module 'discord.js' { TextChannel: typeof TextChannel; VoiceChannel: typeof VoiceChannel; CategoryChannel: typeof CategoryChannel; - GuildChannel: typeof GuildChannel; GuildMember: typeof GuildMember; Guild: typeof Guild; Message: typeof Message; From c078682722541713c9ac30f6777d6dafe560fcb5 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 5 Apr 2019 10:09:58 +0100 Subject: [PATCH 1007/1359] feat(Webhook): add url getter (#3178) * add Webhook#url * set typing as readonly * suggested change * another one --- src/structures/Webhook.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 472de07ea..1ad7cba93 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -213,6 +213,15 @@ class Webhook { return this.client.api.webhooks(this.id, this.token).delete({ reason }); } + /** + * The url of this webhook + * @type {string} + * @readonly + */ + get url() { + return this.client.options.http.api + this.client.api.webhooks(this.id, this.token); + } + static applyToClass(structure) { for (const prop of [ 'send', diff --git a/typings/index.d.ts b/typings/index.d.ts index f1547c876..53857b338 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1270,6 +1270,7 @@ declare module 'discord.js' { public guildID: Snowflake; public name: string; public owner: User | object; + public readonly url: string; } export class WebhookClient extends WebhookMixin(BaseClient) { From 5e9bd786d13f2da11544e45e4ac9622269259789 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 5 Apr 2019 04:32:19 -0500 Subject: [PATCH 1008/1359] refactor(APIRequest): utilize URLSearchParams (#3180) * utilize URLSearchParams * options.query can be undefined/null * oops * remembered what I intended --- src/client/Client.js | 12 ++++++++---- src/rest/APIRequest.js | 10 +++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index 73767f696..b9a20659d 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -338,11 +338,15 @@ class Client extends BaseClient { * .then(link => console.log(`Generated bot invite link: ${link}`)) * .catch(console.error); */ - generateInvite(permissions) { + async generateInvite(permissions) { permissions = Permissions.resolve(permissions); - return this.fetchApplication().then(application => - `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot` - ); + const application = await this.fetchApplication(); + const query = new URLSearchParams({ + client_id: application.id, + permissions: permissions, + scope: 'bot', + }); + return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`; } toJSON() { diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 7b0a73e41..2001fb05c 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -1,6 +1,5 @@ 'use strict'; -const querystring = require('querystring'); const FormData = require('form-data'); const https = require('https'); const { browser, UserAgent } = require('../util/Constants'); @@ -16,8 +15,13 @@ class APIRequest { this.route = options.route; this.options = options; - const queryString = (querystring.stringify(options.query).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); - this.path = `${path}${queryString ? `?${queryString}` : ''}`; + let queryString = ''; + if (options.query) { + // Filter out undefined query options + const query = Object.entries(options.query).filter(([, value]) => typeof value !== 'undefined'); + queryString = new URLSearchParams(query).toString(); + } + this.path = `${path}${queryString && `?${queryString}`}`; } make() { From bfab203934395ebd8884b3fb6360d2d44a0778dd Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 Apr 2019 14:46:25 +0200 Subject: [PATCH 1009/1359] fix(ShardingManager): do not spawn the last shard early An off-by-one error resulted in the last shard getting the delay of the second last one. Closes #3181 --- src/sharding/ShardingManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index aca3ebed3..80cf71d3f 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -194,7 +194,7 @@ class ShardingManager extends EventEmitter { const promises = []; const shard = this.createShard(shardID); promises.push(shard.spawn(waitForReady)); - if (delay > 0 && this.shards.size !== this.shardList.length - 1) promises.push(Util.delayFor(delay)); + if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } From 00eb7e325a4bbc41faa7411a26c3c82cf60d83c1 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 5 Apr 2019 16:32:50 +0200 Subject: [PATCH 1010/1359] fix(ApiRequest): filter out null query values Fixes #3183 --- src/rest/APIRequest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 2001fb05c..672cccd4e 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -18,7 +18,7 @@ class APIRequest { let queryString = ''; if (options.query) { // Filter out undefined query options - const query = Object.entries(options.query).filter(([, value]) => typeof value !== 'undefined'); + const query = Object.entries(options.query).filter(([, value]) => value !== null && typeof value !== 'undefined'); queryString = new URLSearchParams(query).toString(); } this.path = `${path}${queryString && `?${queryString}`}`; From 982f48ce6adff982884fc43ea9eb5d041d219bf0 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 7 Apr 2019 10:57:11 +0300 Subject: [PATCH 1011/1359] src: Fix TypeError --- src/client/voice/VoiceBroadcast.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 5446b46cc..406e14ef0 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -10,7 +10,7 @@ const PlayInterface = require('./util/PlayInterface'); * * Example usage: * ```js - * const broadcast = client.createVoiceBroadcast(); + * const broadcast = client.voice.createBroadcast(); * broadcast.play('./music.mp3'); * // Play "music.mp3" in all voice connections that the client is in * for (const connection of client.voiceConnections.values()) { @@ -79,7 +79,7 @@ class VoiceBroadcast extends EventEmitter { * @event VoiceBroadcast#subscribe * @param {StreamDispatcher} dispatcher The subscribed dispatcher */ - this.broadcast.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); + this.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); return true; } else { return false; @@ -96,7 +96,7 @@ class VoiceBroadcast extends EventEmitter { * @event VoiceBroadcast#unsubscribe * @param {StreamDispatcher} dispatcher The unsubscribed dispatcher */ - this.broadcast.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); + this.emit(Events.VOICE_BROADCAST_UNSUBSCRIBE, dispatcher); return true; } return false; From 152d2e88bd94371e20df4702d08c139f6861ea02 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 8 Apr 2019 13:06:23 +0100 Subject: [PATCH 1012/1359] refactor(WebSocket): utilize URLSearchParams (#3185) * replace querystring with URLSearchParams * looks a bit nicer using urlSearchParams.set(...) --- src/WebSocket.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index 2a303cc46..c8f73972a 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -1,7 +1,6 @@ 'use strict'; const { browser } = require('./util/Constants'); -const querystring = require('querystring'); try { var erlpack = require('erlpack'); if (!erlpack.pack) erlpack = null; @@ -30,8 +29,9 @@ exports.unpack = data => { exports.create = (gateway, query = {}, ...args) => { const [g, q] = gateway.split('?'); query.encoding = exports.encoding; - if (q) query = Object.assign(querystring.parse(q), query); - const ws = new exports.WebSocket(`${g}?${querystring.stringify(query)}`, ...args); + query = new URLSearchParams(query); + if (q) new URLSearchParams(q).forEach((v, k) => query.set(k, v)); + const ws = new exports.WebSocket(`${g}?${query}`, ...args); if (browser) ws.binaryType = 'arraybuffer'; return ws; }; From 70d4b4455b4b7687c7e00aae8f6db5f453f2c7d2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 8 Apr 2019 15:20:53 +0300 Subject: [PATCH 1013/1359] refactor(ClientVoiceManager): make public, remove Client#voiceConnections (#3186) * docs: make voice public * typings: Update typings to match the docs * typings: ClientVoiceManager is nullable in Client Co-Authored-By: vladfrangu * typings: Mark client as readonly Co-Authored-By: vladfrangu * src: Make the client readonly * src: Remove Client#voiceConnections getter in favor of ClientVoiceManager#connections --- src/client/Client.js | 11 ----------- src/client/voice/ClientVoiceManager.js | 4 +++- typings/index.d.ts | 20 ++++++++++++++------ 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index b9a20659d..bdbc442cc 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -86,7 +86,6 @@ class Client extends BaseClient { /** * The voice manager of the client (`null` in browsers) * @type {?ClientVoiceManager} - * @private */ this.voice = !browser ? new ClientVoiceManager(this) : null; @@ -157,16 +156,6 @@ class Client extends BaseClient { } } - /** - * All active voice connections that have been established, mapped by guild ID - * @type {Collection} - * @readonly - */ - get voiceConnections() { - if (browser) return new Collection(); - return this.voice.connections; - } - /** * All custom emojis that the client has access to, mapped by their IDs * @type {GuildEmojiStore} diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index fa3513643..6986c34a6 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -14,8 +14,10 @@ class ClientVoiceManager { /** * The client that instantiated this voice manager * @type {Client} + * @readonly + * @name ClientVoiceManager#client */ - this.client = client; + Object.defineProperty(this, 'client', { value: client }); /** * A collection mapping connection IDs to the Connection objects diff --git a/typings/index.d.ts b/typings/index.d.ts index 53857b338..4af8827c3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -131,11 +131,9 @@ declare module 'discord.js' { export class Client extends BaseClient { constructor(options?: ClientOptions); private actions: object; - private voice: object; private _eval(script: string): any; private _validateOptions(options?: ClientOptions): void; - public broadcasts: VoiceBroadcast[]; public channels: ChannelStore; public readonly emojis: GuildEmojiStore; public guilds: GuildStore; @@ -146,9 +144,8 @@ declare module 'discord.js' { public readonly uptime: number; public user: ClientUser | null; public users: UserStore; - public readonly voiceConnections: Collection; + public voice: ClientVoiceManager | null; public ws: WebSocketManager; - public createVoiceBroadcast(): VoiceBroadcast; public destroy(): void; public fetchApplication(): Promise; public fetchInvite(invite: InviteResolvable): Promise; @@ -234,6 +231,15 @@ declare module 'discord.js' { public once(event: string, listener: Function): this; } + export class ClientVoiceManager { + constructor(client: Client); + public readonly client: Client; + public connections: Collection; + public broadcasts: VoiceBroadcast[]; + + public createBroadcast(): VoiceBroadcast; + } + export class ClientApplication extends Base { constructor(client: Client, data: object); public botPublic?: boolean; @@ -1114,6 +1120,7 @@ declare module 'discord.js' { class VoiceBroadcast extends EventEmitter { constructor(client: Client); public client: Client; + public dispatchers: StreamDispatcher[]; public readonly dispatcher: BroadcastDispatcher; public play(input: string | Readable, options?: StreamOptions): BroadcastDispatcher; @@ -1148,10 +1155,11 @@ declare module 'discord.js' { } class VoiceConnection extends EventEmitter { - constructor(voiceManager: object, channel: VoiceChannel); + constructor(voiceManager: ClientVoiceManager, channel: VoiceChannel); private authentication: object; private sockets: object; private ssrcMap: Map; + private _speaking: Map>; private _disconnect(): void; private authenticate(): void; private authenticateFailed(reason: string): void; @@ -1175,7 +1183,7 @@ declare module 'discord.js' { public receiver: VoiceReceiver; public speaking: Readonly; public status: VoiceStatus; - public voiceManager: object; + public voiceManager: ClientVoiceManager; public disconnect(): void; public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; From 89e27e507144dd3c32d644e0f298207eb538ff65 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 8 Apr 2019 19:29:19 +0300 Subject: [PATCH 1014/1359] src: Make broadcasts work again (#3190) --- src/client/voice/VoiceBroadcast.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 406e14ef0..e8751c05e 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -74,6 +74,7 @@ class VoiceBroadcast extends EventEmitter { add(dispatcher) { const index = this.dispatchers.indexOf(dispatcher); if (index === -1) { + this.dispatchers.push(dispatcher); /** * Emitted whenever a stream dispatcher subscribes to the broadcast. * @event VoiceBroadcast#subscribe From 6be5051f9296fe9a10549f2f9a11c18776b1b617 Mon Sep 17 00:00:00 2001 From: Reseq64 <47262222+Reseq64@users.noreply.github.com> Date: Wed, 10 Apr 2019 17:07:50 +0200 Subject: [PATCH 1015/1359] typo(RequestHandler): fix spelling of 'requests' (#3196) Removed the additional "s" to "requessts" --- src/rest/RequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 4e10b5feb..8f650b42a 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -163,7 +163,7 @@ class RequestHandler { return this.run(); } } else { - // Handle possible malformed requessts + // Handle possible malformed requests try { const data = await parseResponse(res); if (res.status >= 400 && res.status < 500) { From 266ac1c659d9b40a8a7a944e26daf3cee2381b1c Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Wed, 10 Apr 2019 14:40:22 -0500 Subject: [PATCH 1016/1359] docs(Role): fix setPosition's reason type (#3198) --- src/structures/Role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index 87cca568a..45b4ed317 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -298,7 +298,7 @@ class Role extends Base { * @param {number} position The position of the role * @param {Object} [options] Options for setting position * @param {boolean} [options.relative=false] Change the position relative to its current value - * @param {boolean} [options.reason] Reason for changing the position + * @param {string} [options.reason] Reason for changing the position * @returns {Promise} * @example * // Set the position of the role From 62cba2e14850645254cb345d36ac5fb19c76a22a Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Thu, 11 Apr 2019 09:10:31 +0100 Subject: [PATCH 1017/1359] docs(GuildChannel): fix setPosition's reason type (#3199) --- src/structures/GuildChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 9904e4c14..694fdf02e 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -391,7 +391,7 @@ class GuildChannel extends Channel { * @param {number} position The new position for the guild channel * @param {Object} [options] Options for setting position * @param {boolean} [options.relative=false] Change the position relative to its current value - * @param {boolean} [options.reason] Reason for changing the position + * @param {string} [options.reason] Reason for changing the position * @returns {Promise} * @example * // Set a new channel position From 8da141637cc43fdfa592225c121063159b716bac Mon Sep 17 00:00:00 2001 From: thomasxd24 Date: Thu, 11 Apr 2019 22:34:13 +0200 Subject: [PATCH 1018/1359] fix end method in VoiceBroadcast (#3194) * fix end method in VoiceBroadcast this.client is a ClientVoiceManager and thus its this.client.broadcasts instead of this.client.voice.broascasts * revert the voicebroadcast and pass this.client at clientvoice * passed this.client --- src/client/voice/ClientVoiceManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 6986c34a6..2ca2b465a 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -37,7 +37,7 @@ class ClientVoiceManager { * @returns {VoiceBroadcast} */ createBroadcast() { - const broadcast = new VoiceBroadcast(this); + const broadcast = new VoiceBroadcast(this.client); this.broadcasts.push(broadcast); return broadcast; } From 97c196ca6ab1593101e20371914ca41dc7d2dae3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 13 Apr 2019 19:32:43 +0200 Subject: [PATCH 1019/1359] docs(GuildEmoji): add @ name to requiresColons and managed properties --- src/structures/GuildEmoji.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 24fd7c02a..ff705e591 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -29,12 +29,14 @@ class GuildEmoji extends Emoji { /** * Whether or not this emoji requires colons surrounding it * @type {boolean} + * @name GuildEmoji#requiresColons */ if (typeof data.require_colons !== 'undefined') this.requiresColons = data.require_colons; /** * Whether this emoji is managed by an external service * @type {boolean} + * @name GuildEmoji#managed */ if (typeof data.managed !== 'undefined') this.managed = data.managed; From 5d10585af8f41cc5fbf49172a9ce143a4ad34f62 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 14 Apr 2019 13:43:01 +0100 Subject: [PATCH 1020/1359] docs(Presence): add missing descriptions to clientStatus (#3127) * add description on jsdocs for User.clientStatus * Update src/structures/Presence.js Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * Update src/structures/Presence.js Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * Update src/structures/Presence.js Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * update typings --- src/structures/Presence.js | 6 +++--- typings/index.d.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 5d82ac07e..a2d76bc09 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -77,9 +77,9 @@ class Presence { /** * The devices this presence is on * @type {?object} - * @property {PresenceStatus} web - * @property {PresenceStatus} mobile - * @property {PresenceStatus} desktop + * @property {?PresenceStatus} web The current presence in the web application + * @property {?PresenceStatus} mobile The current presence in the mobile application + * @property {?PresenceStatus} desktop The current presence in the desktop application */ this.clientStatus = data.client_status || null; diff --git a/typings/index.d.ts b/typings/index.d.ts index 4af8827c3..172d0dc82 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -813,7 +813,7 @@ declare module 'discord.js' { public activity: Activity; public flags: Readonly; public status: PresenceStatus; - public clientStatus: ClientPresenceStatusData; + public clientStatus: ClientPresenceStatusData | null; public readonly user: User; public readonly member?: GuildMember; public equals(presence: Presence): boolean; @@ -2073,9 +2073,9 @@ declare module 'discord.js' { type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; interface ClientPresenceStatusData { - web?: ClientPresenceStatus; - mobile?: ClientPresenceStatus; - desktop?: ClientPresenceStatus; + web?: PresenceStatus; + mobile?: PresenceStatus; + desktop?: PresenceStatus; } type PartialTypes = 'USER' From ca43919642a80e1b332af88fac5a2d34f67a8b3d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 14 Apr 2019 14:50:55 +0200 Subject: [PATCH 1021/1359] docs: document constructors of extendible structures (#3160) * docs: document constructors of extendible structures * docs(ClientPresence): document default value for data parameter Co-Authored-By: SpaceEEC * docs(Presence): document default value for data parameter Co-Authored-By: SpaceEEC * docs(DMChannel): capitalize DM in the constructor doc --- src/structures/ClientPresence.js | 4 ++++ src/structures/DMChannel.js | 4 ++++ src/structures/Guild.js | 4 ++++ src/structures/GuildChannel.js | 4 ++++ src/structures/GuildEmoji.js | 5 +++++ src/structures/GuildMember.js | 5 +++++ src/structures/Message.js | 5 +++++ src/structures/MessageReaction.js | 5 +++++ src/structures/Presence.js | 4 ++++ src/structures/Role.js | 5 +++++ src/structures/TextChannel.js | 4 ++++ src/structures/User.js | 4 ++++ src/structures/VoiceState.js | 4 ++++ typings/index.d.ts | 2 +- 14 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index a92dfb302..90db53bd0 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -6,6 +6,10 @@ const { ActivityTypes, OPCodes } = require('../util/Constants'); const { TypeError } = require('../errors'); class ClientPresence extends Presence { + /** + * @param {Client} client The instantiating client + * @param {Object} [data={}] The data for the client presence + */ constructor(client, data = {}) { super(client, Object.assign(data, { status: 'online', user: { id: null } })); } diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index e58942c3e..e172f2260 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -10,6 +10,10 @@ const MessageStore = require('../stores/MessageStore'); * @implements {TextBasedChannel} */ class DMChannel extends Channel { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the DM channel + */ constructor(client, data) { super(client, data); // Override the channel type so partials have a known type diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9c44b1b8c..9aeb30973 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -26,6 +26,10 @@ const { Error, TypeError } = require('../errors'); * @extends {Base} */ class Guild extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild + */ constructor(client, data) { super(client); diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 694fdf02e..4ba8eb99b 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -14,6 +14,10 @@ const { Error, TypeError } = require('../errors'); * @extends {Channel} */ class GuildChannel extends Channel { + /** + * @param {Guild} guild The guild the guild channel is part of + * @param {Object} data The data for the guild channel + */ constructor(guild, data) { super(guild.client, data); diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index ff705e591..30c672636 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -10,6 +10,11 @@ const Emoji = require('./Emoji'); * @extends {Emoji} */ class GuildEmoji extends Emoji { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild emoji + * @param {Guild} guild The guild the guild emoji is part of + */ constructor(client, data, guild) { super(client, data); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index d479dd772..e0abea443 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -15,6 +15,11 @@ const { Error } = require('../errors'); * @extends {Base} */ class GuildMember extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the guild member + * @param {Guild} guild The guild the member is part of + */ constructor(client, data, guild) { super(client); diff --git a/src/structures/Message.js b/src/structures/Message.js index afdb5e2c4..b57f4299e 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -19,6 +19,11 @@ const APIMessage = require('./APIMessage'); * @extends {Base} */ class Message extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the message + * @param {TextChannel|DMChannel} channel The channel the message was sent in + */ constructor(client, data, channel) { super(client); diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 69138fda2..fe10e428f 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -9,6 +9,11 @@ const ReactionUserStore = require('../stores/ReactionUserStore'); * Represents a reaction to a message. */ class MessageReaction { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the message reaction + * @param {Message} message The message the reaction refers to + */ constructor(client, data, message) { /** * The message that this reaction refers to diff --git a/src/structures/Presence.js b/src/structures/Presence.js index a2d76bc09..16b4f002c 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -25,6 +25,10 @@ const { ActivityTypes } = require('../util/Constants'); * Represents a user's presence. */ class Presence { + /** + * @param {Client} client The instantiating client + * @param {Object} [data={}] The data for the presence + */ constructor(client, data = {}) { Object.defineProperty(this, 'client', { value: client }); /** diff --git a/src/structures/Role.js b/src/structures/Role.js index 45b4ed317..ce9f36373 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -11,6 +11,11 @@ const { Error, TypeError } = require('../errors'); * @extends {Base} */ class Role extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the role + * @param {Guild} guild The guild the role is part of + */ constructor(client, data, guild) { super(client); diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index a643815e5..e50bee05a 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -13,6 +13,10 @@ const MessageStore = require('../stores/MessageStore'); * @implements {TextBasedChannel} */ class TextChannel extends GuildChannel { + /** + * @param {Guild} guild The guild the text channel is part of + * @param {Object} data The data for the text channel + */ constructor(guild, data) { super(guild, data); /** diff --git a/src/structures/User.js b/src/structures/User.js index a0436f7e8..c2276c4f2 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -12,6 +12,10 @@ const { Error } = require('../errors'); * @extends {Base} */ class User extends Base { + /** + * @param {Client} client The instantiating client + * @param {Object} data The data for the user + */ constructor(client, data) { super(client); diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 9ff71e87a..d74fc6aa0 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -6,6 +6,10 @@ const Base = require('./Base'); * Represents the voice state for a Guild Member. */ class VoiceState extends Base { + /** + * @param {Guild} guild The guild the voice state is part of + * @param {Object} data The data for the voice state + */ constructor(guild, data) { super(guild.client); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 172d0dc82..3ad8789db 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -809,7 +809,7 @@ declare module 'discord.js' { } export class Presence { - constructor(client: Client, data?: object); + constructor(client: Client, data: object); public activity: Activity; public flags: Readonly; public status: PresenceStatus; From 520810d48411013307ca310f0eff988a210266ca Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 14 Apr 2019 13:58:33 +0100 Subject: [PATCH 1022/1359] feat(Util): add YELLOW to ColorResolvable (#3182) --- src/util/Constants.js | 1 + src/util/Util.js | 1 + typings/index.d.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index dfd370f71..f314d3d6f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -422,6 +422,7 @@ exports.Colors = { AQUA: 0x1ABC9C, GREEN: 0x2ECC71, BLUE: 0x3498DB, + YELLOW: 0xFFFF00, PURPLE: 0x9B59B6, LUMINOUS_VIVID_PINK: 0xE91E63, GOLD: 0xF1C40F, diff --git a/src/util/Util.js b/src/util/Util.js index bbf838e46..700c519bc 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -251,6 +251,7 @@ class Util { * - `AQUA` * - `GREEN` * - `BLUE` + * - `YELLOW` * - `PURPLE` * - `LUMINOUS_VIVID_PINK` * - `GOLD` diff --git a/typings/index.d.ts b/typings/index.d.ts index 3ad8789db..673104f7d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1685,9 +1685,11 @@ declare module 'discord.js' { } type ColorResolvable = 'DEFAULT' + | 'WHITE' | 'AQUA' | 'GREEN' | 'BLUE' + | 'YELLOW' | 'PURPLE' | 'LUMINOUS_VIVID_PINK' | 'GOLD' From cbb9b14950dd8866557406a014b7b08e4c2acff4 Mon Sep 17 00:00:00 2001 From: Isabella Date: Sun, 14 Apr 2019 08:27:59 -0500 Subject: [PATCH 1023/1359] test: add Webhook(Client) testing file (#2855) * test: add WebhookClient testing file * webhooks cant edit messages, silly * add more webhook types as requested * fix typo * eslint matters * fix(webhooktests): add 'use strict', remove embed: tests --- test/webhooktest.js | 149 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 test/webhooktest.js diff --git a/test/webhooktest.js b/test/webhooktest.js new file mode 100644 index 000000000..2fe11b9df --- /dev/null +++ b/test/webhooktest.js @@ -0,0 +1,149 @@ +'use strict'; + +const Discord = require('../src'); + +const { owner, token, webhookChannel, webhookToken } = require('./auth.js'); + +const client = new Discord.Client(); + +const fetch = require('node-fetch'); +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const fill = c => Array(4).fill(c.repeat(1000)); +const buffer = l => fetch(l).then(res => res.buffer()); +const read = util.promisify(fs.readFile); +const readStream = fs.createReadStream; +const wait = util.promisify(setTimeout); + +const linkA = 'https://lolisafe.moe/iiDMtAXA.png'; +const linkB = 'https://lolisafe.moe/9hSpedPh.png'; +const fileA = path.join(__dirname, 'blobReach.png'); + +const embed = () => new Discord.MessageEmbed(); +const attach = (attachment, name) => new Discord.MessageAttachment(attachment, name); + +const tests = [ + (m, hook) => hook.send('x'), + (m, hook) => hook.send(['x', 'y']), + + (m, hook) => hook.send('x', { code: true }), + (m, hook) => hook.send('1', { code: 'js' }), + (m, hook) => hook.send('x', { code: '' }), + + (m, hook) => hook.send(fill('x'), { split: true }), + (m, hook) => hook.send(fill('1'), { code: 'js', split: true }), + (m, hook) => hook.send(fill('x'), { reply: m.author, code: 'js', split: true }), + (m, hook) => hook.send(fill('xyz '), { split: { char: ' ' } }), + + (m, hook) => hook.send({ embeds: [{ description: 'a' }] }), + (m, hook) => hook.send({ files: [{ attachment: linkA }] }), + (m, hook) => hook.send({ + embeds: [{ description: 'a' }], + files: [{ attachment: linkA, name: 'xyz.png' }], + }), + + (m, hook) => hook.send('x', embed().setDescription('a')), + (m, hook) => hook.send(embed().setDescription('a')), + (m, hook) => hook.send({ embeds: [embed().setDescription('a')] }), + (m, hook) => hook.send([embed().setDescription('a'), embed().setDescription('b')]), + + (m, hook) => hook.send('x', attach(linkA)), + (m, hook) => hook.send(attach(linkA)), + (m, hook) => hook.send({ files: [linkA] }), + (m, hook) => hook.send({ files: [attach(linkA)] }), + async (m, hook) => hook.send(attach(await buffer(linkA))), + async (m, hook) => hook.send({ files: [await buffer(linkA)] }), + async (m, hook) => hook.send({ files: [{ attachment: await buffer(linkA) }] }), + (m, hook) => hook.send([attach(linkA), attach(linkB)]), + + (m, hook) => hook.send(embed().setDescription('a')), + + (m, hook) => hook.send({ embeds: [{ description: 'a' }] }), + (m, hook) => hook.send(embed().setDescription('a')), + + (m, hook) => hook.send(['x', 'y'], [embed().setDescription('a'), attach(linkB)]), + (m, hook) => hook.send(['x', 'y'], [attach(linkA), attach(linkB)]), + + (m, hook) => hook.send([embed().setDescription('a'), attach(linkB)]), + (m, hook) => hook.send({ + embeds: [embed().setImage('attachment://two.png')], + files: [attach(linkB, 'two.png')], + }), + (m, hook) => hook.send({ + embeds: [ + embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + ], + }), + async (m, hook) => hook.send(['x', 'y', 'z'], { + code: 'js', + embeds: [ + embed() + .setImage('attachment://two.png') + .attachFiles([attach(linkB, 'two.png')]), + ], + files: [{ attachment: await buffer(linkA) }], + }), + + (m, hook) => hook.send('x', attach(fileA)), + (m, hook) => hook.send({ files: [fileA] }), + (m, hook) => hook.send(attach(fileA)), + async (m, hook) => hook.send({ files: [await read(fileA)] }), + async (m, hook) => hook.send(fill('x'), { + reply: m.author, + code: 'js', + split: true, + embeds: [embed().setImage('attachment://zero.png')], + files: [attach(await buffer(linkA), 'zero.png')], + }), + + (m, hook) => hook.send('x', attach(readStream(fileA))), + (m, hook) => hook.send({ files: [readStream(fileA)] }), + (m, hook) => hook.send({ files: [{ attachment: readStream(fileA) }] }), + async (m, hook) => hook.send(fill('xyz '), { + reply: m.author, + code: 'js', + split: { char: ' ', prepend: 'hello! ', append: '!!!' }, + embeds: [embed().setImage('attachment://zero.png')], + files: [linkB, attach(await buffer(linkA), 'zero.png'), readStream(fileA)], + }), + + (m, hook) => hook.send('Done!'), +]; + + +client.on('message', async message => { + if (message.author.id !== owner) return; + const match = message.content.match(/^do (.+)$/); + const hooks = [ + { type: 'WebhookClient', hook: new Discord.WebhookClient(webhookChannel, webhookToken) }, + { type: 'TextChannel#fetchWebhooks', hook: await message.channel.fetchWebhooks().then(x => x.first()) }, + { type: 'Guild#fetchWebhooks', hook: await message.guild.fetchWebhooks().then(x => x.first()) }, + ]; + if (match && match[1] === 'it') { + /* eslint-disable no-await-in-loop */ + for (const { type, hook } of hooks) { + for (const [i, test] of tests.entries()) { + await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message, hook).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + await wait(1000); + } + } + /* eslint-enable no-await-in-loop */ + } else if (match) { + const n = parseInt(match[1]) || 0; + const test = tests.slice(n)[0]; + const i = tests.indexOf(test); + /* eslint-disable no-await-in-loop */ + for (const { type, hook } of hooks) { + await message.channel.send(`**#${i}-Hook: ${type}**\n\`\`\`js\n${test.toString()}\`\`\``); + await test(message, hook).catch(e => message.channel.send(`Error!\n\`\`\`\n${e}\`\`\``)); + } + /* eslint-enable no-await-in-loop */ + } +}); + +client.login(token); From 2af101efbb0fe5c014727dc130a6307066395fe7 Mon Sep 17 00:00:00 2001 From: danielnewell <46284599+danielnewell@users.noreply.github.com> Date: Sun, 14 Apr 2019 09:47:52 -0400 Subject: [PATCH 1024/1359] docs(faq): add a link to the guide, restructure a bit (#3082) * Update faq.md added link to guide * docs(faq): link documentation as per suggestion --- docs/general/faq.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/general/faq.md b/docs/general/faq.md index 950f4d9ca..212a16d84 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -1,7 +1,6 @@ # Frequently Asked Questions -These are just questions that get asked frequently, that usually have a common resolution. -If you have issues not listed here, please ask in the [official Discord server](https://discord.gg/bRCvFy9). -Always make sure to read the documentation. +These questions are some of the most frequently asked. + ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ Update to Node.js 8.0.0 or newer. @@ -21,3 +20,7 @@ Update to Node.js 8.0.0 or newer. - **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats! - **Windows:** Run `npm install --global --production windows-build-tools` in an admin command prompt or PowerShell. Then, running `npm install node-opus` in your bot's directory should successfully build it. Woo! + +Other questions can be found at the [official Discord.js guide](https://discordjs.guide/popular-topics/common-questions.html) +If you have issues not listed here or on the guide, feel free to ask in the [official Discord.js server](https://discord.gg/bRCvFy9). +Always make sure to read the [documentation](https://discord.js.org/#/docs/main/stable/general/welcome). From d9a053df67b780b85e09657345bfa7c06c9cf41b Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 15 Apr 2019 13:41:37 +0100 Subject: [PATCH 1025/1359] docs(Presence): add ClientPresenceStatus typedef (#3208) --- src/structures/Presence.js | 17 ++++++++++++----- typings/index.d.ts | 6 +++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 16b4f002c..55937cea4 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -13,7 +13,6 @@ const { ActivityTypes } = require('../util/Constants'); /** * The status of this presence: - * * * **`online`** - user is online * * **`idle`** - user is AFK * * **`offline`** - user is offline or invisible @@ -21,6 +20,14 @@ const { ActivityTypes } = require('../util/Constants'); * @typedef {string} PresenceStatus */ +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} ClientPresenceStatus + */ + /** * Represents a user's presence. */ @@ -80,10 +87,10 @@ class Presence { /** * The devices this presence is on - * @type {?object} - * @property {?PresenceStatus} web The current presence in the web application - * @property {?PresenceStatus} mobile The current presence in the mobile application - * @property {?PresenceStatus} desktop The current presence in the desktop application + * @type {?Object} + * @property {?ClientPresenceStatus} web The current presence in the web application + * @property {?ClientPresenceStatus} mobile The current presence in the mobile application + * @property {?ClientPresenceStatus} desktop The current presence in the desktop application */ this.clientStatus = data.client_status || null; diff --git a/typings/index.d.ts b/typings/index.d.ts index 673104f7d..fd680f05a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2075,9 +2075,9 @@ declare module 'discord.js' { type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; interface ClientPresenceStatusData { - web?: PresenceStatus; - mobile?: PresenceStatus; - desktop?: PresenceStatus; + web?: ClientPresenceStatus; + mobile?: ClientPresenceStatus; + desktop?: ClientPresenceStatus; } type PartialTypes = 'USER' From eb537b6f4896d84cc660e95be8685223e8f9c785 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 15 Apr 2019 21:17:27 +0300 Subject: [PATCH 1026/1359] docs(WebSocketShard): mark non-nullable parameters as non-nullable (#3209) * docs: Imagine having an optional nullable param * docs: Another one --- src/client/websocket/WebSocketShard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 19cb11e4b..c927c1561 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -526,7 +526,7 @@ class WebSocketShard extends EventEmitter { * a full [Payload](https://discordapp.com/developers/docs/topics/gateway#commands-and-events-gateway-commands). * Do not use this method if you don't know what you're doing. * @param {Object} data The full packet to send - * @param {?boolean} [important=false] If this packet should be added first in queue + * @param {boolean} [important=false] If this packet should be added first in queue */ send(data, important = false) { this.ratelimit.queue[important ? 'unshift' : 'push'](data); @@ -574,7 +574,7 @@ class WebSocketShard extends EventEmitter { /** * Destroys this shard and closes its WebSocket connection. - * @param {?number} [closeCode=1000] The close code to use + * @param {number} [closeCode=1000] The close code to use * @private */ destroy(closeCode = 1000) { From 52bc5b0170e290291d9760c13a1969874f936e6d Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Mon, 15 Apr 2019 19:46:59 +0100 Subject: [PATCH 1027/1359] feat(MessageEmbed): resolve color in embed constructor (#2912) * Resolve color in embed constructor * Use ColorResolvable type for color parameter * docs(MessageEmbed): color property is still a number --- src/structures/MessageEmbed.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 8276cad77..706b20e91 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -45,7 +45,7 @@ class MessageEmbed { * The color of this embed * @type {?number} */ - this.color = data.color; + this.color = Util.resolveColor(data.color); /** * The timestamp of this embed diff --git a/typings/index.d.ts b/typings/index.d.ts index fd680f05a..fe15fa897 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1965,7 +1965,7 @@ declare module 'discord.js' { description?: string; url?: string; timestamp?: Date | number; - color?: number | string; + color?: ColorResolvable; fields?: { name: string; value: string; inline?: boolean; }[]; files?: (MessageAttachment | string | FileOptions)[]; author?: { name?: string; url?: string; icon_url?: string; iconURL?: string; }; From 0b1176d9a1078d056835dc4e573fe639d4c5ed79 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 15 Apr 2019 20:01:39 +0100 Subject: [PATCH 1028/1359] chore(typings): declaring explicit nullable returns (#3134) * strict nullable & WebSocketManager private typings * missing semicolon * kyra suggestion #1 Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * kyra suggestion #2&3 * space's requested changes * space's requested change * strict nullable & WebSocketManager private typings missing semicolon kyra suggestion #1 Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> kyra suggestion #2&3 space's requested changes space's requested change * resolve conflicts * deflate --- typings/index.d.ts | 207 +++++++++++++++++++++++---------------------- 1 file changed, 105 insertions(+), 102 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index fe15fa897..0a2a12484 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -10,21 +10,21 @@ declare module 'discord.js' { export class Activity { constructor(presence: Presence, data?: object); - public applicationID: Snowflake; - public assets: RichPresenceAssets; - public details: string; + public applicationID: Snowflake | null; + public assets: RichPresenceAssets | null; + public details: string | null; public name: string; public party: { - id: string; + id: string | null; size: [number, number]; - }; - public state: string; + } | null; + public state: string | null; public timestamps: { - start: Date; - end: Date; - }; + start: Date | null; + end: Date | null; + } | null; public type: ActivityType; - public url: string; + public url: string | null; public equals(activity: Activity): boolean; } @@ -35,10 +35,10 @@ declare module 'discord.js' { export class APIMessage { constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions); - public data?: object; + public data: object | null; public readonly isUser: boolean; public readonly isWebhook: boolean; - public files?: object[]; + public files: object[] | null; public options: MessageOptions | WebhookMessageOptions; public target: MessageTarget; @@ -138,10 +138,10 @@ declare module 'discord.js' { public readonly emojis: GuildEmojiStore; public guilds: GuildStore; public readyAt: Date | null; - public readonly readyTimestamp: number; - public shard: ShardClientUtil; - public token: string; - public readonly uptime: number; + public readonly readyTimestamp: number | null; + public shard: ShardClientUtil | null; + public token: string | null; + public readonly uptime: number | null; public user: ClientUser | null; public users: UserStore; public voice: ClientVoiceManager | null; @@ -169,16 +169,16 @@ declare module 'discord.js' { public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; - public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; + public on(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -206,16 +206,16 @@ declare module 'discord.js' { public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; - public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; + public once(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -237,21 +237,23 @@ declare module 'discord.js' { public connections: Collection; public broadcasts: VoiceBroadcast[]; + private joinChannel(channel: VoiceChannel): Promise; + public createBroadcast(): VoiceBroadcast; } export class ClientApplication extends Base { constructor(client: Client, data: object); - public botPublic?: boolean; - public botRequireCodeGrant?: boolean; - public cover?: string; + public botPublic: boolean | null; + public botRequireCodeGrant: boolean | null; + public cover: string | null; public readonly createdAt: Date; public readonly createdTimestamp: number; public description: string; public icon: string; public id: Snowflake; public name: string; - public owner?: User; + public owner: User | null; public rpcOrigins: string[]; public coverImage(options?: AvatarOptions): string; public fetchAssets(): Promise; @@ -290,8 +292,8 @@ declare module 'discord.js' { public equals(collection: Collection): boolean; public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; - public find(fn: (value: V, key: K, collection: Collection) => boolean): V; - public findKey(fn: (value: V, key: K, collection: Collection) => boolean): K; + public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V; + public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K; public first(): V | undefined; public first(count: number): V[]; public firstKey(): K | undefined; @@ -317,7 +319,7 @@ declare module 'discord.js' { export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); - private _timeout: NodeJS.Timer; + private _timeout: NodeJS.Timer | null; public readonly client: Client; public collected: Collection; @@ -391,7 +393,7 @@ declare module 'discord.js' { protected setup(data: any): void; - public readonly afkChannel: VoiceChannel; + public readonly afkChannel: VoiceChannel | null; public afkChannelID: Snowflake; public afkTimeout: number; public applicationID: Snowflake; @@ -399,6 +401,7 @@ declare module 'discord.js' { public channels: GuildChannelStore; public readonly createdAt: Date; public readonly createdTimestamp: number; + public readonly defaultRole: Role | null; public defaultMessageNotifications: DefaultMessageNotifications | number; public readonly defaultRole: Role; public deleted: boolean; @@ -411,13 +414,13 @@ declare module 'discord.js' { public readonly joinedAt: Date; public joinedTimestamp: number; public large: boolean; - public readonly me: GuildMember; + public readonly me: GuildMember | null; public memberCount: number; public members: GuildMemberStore; public mfaLevel: number; public name: string; public readonly nameAcronym: string; - public readonly owner: GuildMember; + public readonly owner: GuildMember | null; public ownerID: Snowflake; public presences: PresenceStore; public region: string; @@ -425,11 +428,11 @@ declare module 'discord.js' { public readonly shard: WebSocketShard; public shardID: number; public splash: string; - public readonly systemChannel: TextChannel; + public readonly systemChannel: TextChannel | null; public systemChannelID: Snowflake; public verificationLevel: number; public readonly verified: boolean; - public readonly voiceConnection: VoiceConnection; + public readonly voiceConnection: VoiceConnection | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; @@ -483,13 +486,13 @@ declare module 'discord.js' { constructor(logs: GuildAuditLogs, guild: Guild, data: object); public action: GuildAuditLogsAction; public actionType: GuildAuditLogsActionType; - public changes: AuditLogChange[]; + public changes: AuditLogChange[] | null; public readonly createdAt: Date; public readonly createdTimestamp: number; public executor: User; - public extra: object | Role | GuildMember; + public extra: object | Role | GuildMember | null; public id: Snowflake; - public reason: string; + public reason: string | null; public target: Guild | User | Role | GuildEmoji | Invite | Webhook; public targetType: GuildAuditLogsTarget; public toJSON(): object; @@ -505,10 +508,10 @@ declare module 'discord.js' { public guild: Guild; public readonly manageable: boolean; public name: string; - public readonly parent: CategoryChannel; + public readonly parent: CategoryChannel | null; public parentID: Snowflake; public permissionOverwrites: Collection; - public readonly permissionsLocked: boolean; + public readonly permissionsLocked: boolean | null; public readonly position: number; public rawPosition: number; public readonly viewable: boolean; @@ -553,8 +556,8 @@ declare module 'discord.js' { public readonly displayName: string; public guild: Guild; public readonly id: Snowflake; - public readonly joinedAt: Date; - public joinedTimestamp: number; + public readonly joinedAt: Date | null; + public joinedTimestamp: number | null; public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string; @@ -612,18 +615,18 @@ declare module 'discord.js' { public channel: GuildChannel; public code: string; public readonly createdAt: Date; - public createdTimestamp: number; - public readonly expiresAt: Date; - public readonly expiresTimestamp: number; - public guild: Guild; - public inviter: User; - public maxAge: number; - public maxUses: number; + public createdTimestamp: number | null; + public readonly expiresAt: Date | null; + public readonly expiresTimestamp: number | null; + public guild: Guild | null; + public inviter: User | null; + public maxAge: number | null; + public maxUses: number | null; public memberCount: number; public presenceCount: number; - public temporary: boolean; + public temporary: boolean | null; public readonly url: string; - public uses: number; + public uses: number | null; public delete(reason?: string): Promise; public toJSON(): object; public toString(): string; @@ -634,10 +637,10 @@ declare module 'discord.js' { private _edits: Message[]; private patch(data: object): void; - public activity: GroupActivity; - public application: ClientApplication; + public activity: GroupActivity | null; + public application: ClientApplication | null; public attachments: Collection; - public author: User; + public author: User | null; public channel: TextChannel | DMChannel; public readonly cleanContent: string; public content: string; @@ -646,13 +649,13 @@ declare module 'discord.js' { public readonly deletable: boolean; public deleted: boolean; public readonly editable: boolean; - public readonly editedAt: Date; - public editedTimestamp: number; + public readonly editedAt: Date | null; + public editedTimestamp: number | null; public readonly edits: Message[]; public embeds: MessageEmbed[]; - public readonly guild: Guild; + public readonly guild: Guild | null; public id: Snowflake; - public readonly member: GuildMember; + public readonly member: GuildMember | null; public mentions: MessageMentions; public nonce: string; public readonly partial: boolean; @@ -663,7 +666,7 @@ declare module 'discord.js' { public tts: boolean; public type: MessageType; public readonly url: string; - public webhookID: Snowflake; + public webhookID: Snowflake | null; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; @@ -685,13 +688,13 @@ declare module 'discord.js' { constructor(attachment: BufferResolvable | Stream, name?: string, data?: object); public attachment: BufferResolvable | Stream; - public height: number; + public height: number | null; public id: Snowflake; public name?: string; public proxyURL: string; public size: number; public url: string; - public width: number; + public width: number | null; public setFile(attachment: BufferResolvable | Stream, name?: string): this; public setName(name: string): this; public toJSON(): object; @@ -712,23 +715,23 @@ declare module 'discord.js' { constructor(data?: MessageEmbed | MessageEmbedOptions); private _apiTransform(): MessageEmbedOptions; - public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string }; + public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string } | null; public color: number; - public readonly createdAt: Date; + public readonly createdAt: Date | null; public description: string; public fields: EmbedField[]; public files: (MessageAttachment | string | FileOptions)[]; - public footer: { text?: string; iconURL?: string; proxyIconURL?: string }; - public readonly hexColor: string; - public image: { url: string; proxyURL?: string; height?: number; width?: number; }; + public footer: { text?: string; iconURL?: string; proxyIconURL?: string } | null; + public readonly hexColor: string | null; + public image: { url: string; proxyURL?: string; height?: number; width?: number; } | null; public readonly length: number; public provider: { name: string; url: string; }; - public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; }; - public timestamp: number; + public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; } | null; + public timestamp: number | null; public title: string; public type: string; public url: string; - public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number }; + public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number } | null; public addBlankField(inline?: boolean): this; public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; @@ -749,9 +752,9 @@ declare module 'discord.js' { export class MessageMentions { constructor(message: Message, users: object[] | Collection, roles: Snowflake[] | Collection, everyone: boolean); - private _channels: Collection; + private _channels: Collection | null; private readonly _content: Message; - private _members: Collection; + private _members: Collection | null; public readonly channels: Collection; public readonly client: Client; @@ -762,7 +765,7 @@ declare module 'discord.js' { ignoreRoles?: boolean; ignoreEveryone?: boolean; }): boolean; - public readonly members: Collection; + public readonly members: Collection | null; public roles: Collection; public users: Collection; public toJSON(): object; @@ -809,13 +812,13 @@ declare module 'discord.js' { } export class Presence { - constructor(client: Client, data: object); - public activity: Activity; + constructor(client: Client, data?: object); + public activity: Activity | null; public flags: Readonly; public status: PresenceStatus; public clientStatus: ClientPresenceStatusData | null; - public readonly user: User; - public readonly member?: GuildMember; + public readonly user: User | null; + public readonly member: GuildMember | null; public equals(presence: Presence): boolean; } @@ -831,7 +834,7 @@ declare module 'discord.js' { public collect(reaction: MessageReaction): Snowflake | string; public dispose(reaction: MessageReaction, user: User): Snowflake | string; public empty(): void; - public endReason(): string; + public endReason(): string | null; public on(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; public on(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; @@ -854,12 +857,12 @@ declare module 'discord.js' { export class RichPresenceAssets { constructor(activity: Activity, assets: object); - public largeImage: Snowflake; - public largeText: string; - public smallImage: Snowflake; - public smallText: string; - public largeImageURL(options: AvatarOptions): string; - public smallImageURL(options: AvatarOptions): string; + public largeImage: Snowflake | null; + public largeText: string | null; + public smallImage: Snowflake | null; + public smallText: string | null; + public largeImageURL(options: AvatarOptions): string | null; + public smallImageURL(options: AvatarOptions): string | null; } export class Role extends Base { @@ -910,9 +913,9 @@ declare module 'discord.js' { public env: object; public id: number; public manager: ShardingManager; - public process: ChildProcess; + public process: ChildProcess | null; public ready: boolean; - public worker: any; + public worker: any | null; public eval(script: string): Promise; public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; @@ -945,7 +948,7 @@ declare module 'discord.js' { public readonly count: number; public readonly ids: number[]; public mode: ShardingManagerMode; - public parentPort: any; + public parentPort: any | null; public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; @@ -969,7 +972,7 @@ declare module 'discord.js' { public respawn: boolean; public shardArgs: string[]; public shards: Collection; - public token: string; + public token: string | null; public totalShards: number | 'auto'; public broadcast(message: any): Promise; public broadcastEval(script: string): Promise; @@ -994,9 +997,9 @@ declare module 'discord.js' { constructor(player: object, options?: StreamOptions, streams?: object); public player: object; public pausedSince: number; - public broadcast: VoiceBroadcast; + public broadcast: VoiceBroadcast | null; public readonly paused: boolean; - public readonly pausedTime: boolean; + public readonly pausedTime: boolean | null; public readonly streamTime: number; public readonly totalStreamTime: number; public readonly bitrateEditable: boolean; @@ -1061,7 +1064,7 @@ declare module 'discord.js' { export class User extends PartialTextBasedChannel(Base) { constructor(client: Client, data: object); - public avatar: string; + public avatar: string | null; public bot: boolean; public readonly createdAt: Date; public readonly createdTimestamp: number; @@ -1074,7 +1077,7 @@ declare module 'discord.js' { public readonly presence: Presence; public readonly tag: string; public username: string; - public avatarURL(options?: AvatarOptions): string; + public avatarURL(options?: AvatarOptions): string | null; public createDM(): Promise; public deleteDM(): Promise; public displayAvatarURL(options?: AvatarOptions): string; @@ -1102,7 +1105,7 @@ declare module 'discord.js' { public static makePlainError(err: Error): { name: string, message: string, stack: string }; public static mergeDefault(def: object, given: object): object; public static moveElementInArray(array: any[], element: any, newIndex: number, offset?: boolean): number; - public static parseEmoji(text: string): { animated: boolean; name: string; id: string; }; + public static parseEmoji(text: string): { animated: boolean; name: string; id: string | null; } | null; public static resolveColor(color: ColorResolvable): number; public static resolveString(data: StringResolvable): string; public static setPosition( @@ -1238,19 +1241,19 @@ declare module 'discord.js' { export class VoiceState extends Base { constructor(guild: Guild, data: object); - public readonly channel?: VoiceChannel; + public readonly channel: VoiceChannel | null; public channelID?: Snowflake; public readonly deaf?: boolean; public guild: Guild; public id: Snowflake; - public readonly member: GuildMember; + public readonly member: GuildMember | null; public readonly mute?: boolean; public selfDeaf?: boolean; public selfMute?: boolean; public serverDeaf?: boolean; public serverMute?: boolean; public sessionID?: string; - public readonly speaking?: boolean; + public readonly speaking: boolean | null; public setDeaf(mute: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; @@ -1277,7 +1280,7 @@ declare module 'discord.js' { public channelID: Snowflake; public guildID: Snowflake; public name: string; - public owner: User | object; + public owner: User | object | null; public readonly url: string; } @@ -1394,7 +1397,7 @@ declare module 'discord.js' { export class GuildEmojiStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; - public resolveIdentifier(emoji: EmojiIdentifierResolvable): string; + public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; } export class GuildChannelStore extends DataStore { @@ -1410,8 +1413,8 @@ declare module 'discord.js' { export class GuildMemberRoleStore extends OverridableDataStore { constructor(member: GuildMember); - public readonly hoist: Role; - public readonly color: Role; + public readonly hoist: Role | null; + public readonly color: Role | null; public readonly highest: Role; public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; @@ -1431,7 +1434,7 @@ declare module 'discord.js' { export class GuildStore extends DataStore { constructor(client: Client, iterable?: Iterable); - public create(name: string, options?: { region?: string, icon?: BufferResolvable | Base64Resolvable }): Promise; + public create(name: string, options?: { region?: string, icon: BufferResolvable | Base64Resolvable | null }): Promise; } export class MessageStore extends DataStore { @@ -1483,10 +1486,10 @@ declare module 'discord.js' { const TextBasedChannel: (Base?: Constructable) => Constructable; interface PartialTextBasedChannelFields { - lastMessageID: Snowflake; - lastMessageChannelID: Snowflake; - readonly lastMessage: Message; - lastPinTimestamp: number; + lastMessageID: Snowflake | null; + lastMessageChannelID: Snowflake | null; + readonly lastMessage: Message | null; + lastPinTimestamp: number | null; readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; @@ -1874,7 +1877,7 @@ declare module 'discord.js' { interface GuildEmbedData { enabled: boolean; - channel?: GuildChannelResolvable; + channel: GuildChannelResolvable | null; } type GuildFeatures = 'INVITE_SPLASH' From 39fd8fd6454aaa61e098ef8fac1a33aeede97f9a Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 16 Apr 2019 21:14:01 +0100 Subject: [PATCH 1029/1359] fix(typings): remove duplicated Guild#defaultRole (#3211) --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0a2a12484..4c32f0b1b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -403,7 +403,6 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public readonly defaultRole: Role | null; public defaultMessageNotifications: DefaultMessageNotifications | number; - public readonly defaultRole: Role; public deleted: boolean; public embedEnabled: boolean; public emojis: GuildEmojiStore; From b5320299f79c5ccfba8dac95699d2d7202682b56 Mon Sep 17 00:00:00 2001 From: "Deivu (Saya)" <36309350+Deivu@users.noreply.github.com> Date: Wed, 17 Apr 2019 21:32:57 +0800 Subject: [PATCH 1030/1359] Only reset sessionID when close code is 1000 or 4006 (#3213) * Event code 1001 should not get its sessionID reset * Reset sessionID when close code is 1000 or 4006 --- src/client/websocket/WebSocketManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index c483e803b..eb4a8282c 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -216,7 +216,7 @@ class WebSocketManager { return; } - if (event.code >= 1000 && event.code <= 2000) { + if (event.code === 1000 || event.code === 4006) { // Any event code in this range cannot be resumed. shard.sessionID = undefined; } From bccbb550b08364df38d7ae8e03d162d8565a76cb Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Fri, 19 Apr 2019 02:47:39 -0400 Subject: [PATCH 1031/1359] docs(Collector): specify the unit for `CollectionOptions#time` (#3219) --- src/structures/interfaces/Collector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 1dbd61429..8e6d8463f 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -15,7 +15,7 @@ const EventEmitter = require('events'); /** * Options to be applied to the collector. * @typedef {Object} CollectorOptions - * @property {number} [time] How long to run the collector for + * @property {number} [time] How long to run the collector for in milliseconds * @property {boolean} [dispose=false] Whether to dispose data when it's deleted */ From cde955c7660d753fc1b51c5f94b4f51c6b0d8533 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 19 Apr 2019 08:49:17 +0200 Subject: [PATCH 1032/1359] fix(PresenceUpdateAction): emit presences again (#3214) * fix(PresenceUpdateAction): emit presences again * update typings --- src/client/actions/PresenceUpdate.js | 40 +++++++++++++--------------- typings/index.d.ts | 6 +++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 649bf6ce0..538789e25 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -5,40 +5,38 @@ const { Events } = require('../../util/Constants'); class PresenceUpdateAction extends Action { handle(data) { - let cached = this.client.users.get(data.user.id); - if (!cached && data.user.username) cached = this.client.users.add(data.user); - if (!cached) return; + let user = this.client.users.get(data.user.id); + if (!user && data.user.username) user = this.client.users.add(data.user); + if (!user) return; if (data.user && data.user.username) { - if (!cached.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); + if (!user.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); } const guild = this.client.guilds.get(data.guild_id); if (!guild) return; - let member = guild.members.get(cached.id); + let oldPresence = guild.presences.get(user.id); + if (oldPresence) oldPresence = oldPresence._clone(); + let member = guild.members.get(user.id); if (!member && data.status !== 'offline') { - member = guild.members.add({ user: cached, roles: data.roles, deaf: false, mute: false }); + member = guild.members.add({ + user, + roles: data.roles, + deaf: false, + mute: false, + }); this.client.emit(Events.GUILD_MEMBER_AVAILABLE, member); } - - if (member) { - if (this.client.listenerCount(Events.PRESENCE_UPDATE) === 0) { - guild.presences.add(data); - return; - } - const old = member._clone(); - if (member.presence) old.frozenPresence = member.presence._clone(); - guild.presences.add(data); + guild.presences.add(Object.assign(data, { guild })); + if (member && this.client.listenerCount(Events.PRESENCE_UPDATE)) { /** - * Emitted whenever a guild member's presence changes, or they change one of their details. + * Emitted whenever a guild member's presence (e.g. status, activity) is changed. * @event Client#presenceUpdate - * @param {GuildMember} oldMember The member before the presence update - * @param {GuildMember} newMember The member after the presence update + * @param {?Presence} oldPresence The presence before the update, if one at all + * @param {Presence} newPresence The presence after the update */ - this.client.emit(Events.PRESENCE_UPDATE, old, member); - } else { - guild.presences.add(data); + this.client.emit(Events.PRESENCE_UPDATE, oldPresence, member.presence); } } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 4c32f0b1b..d1894df89 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -169,13 +169,14 @@ declare module 'discord.js' { public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public on(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; public on(event: 'resume', listener: (replayed: number, shardID: number) => void): this; @@ -206,13 +207,14 @@ declare module 'discord.js' { public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public once(event: 'guildMemberUpdate' | 'presenceUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; public once(event: 'resume', listener: (replayed: number, shardID: number) => void): this; From 1514df0f87cb7fc6cc210b00db977414c39665df Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 21 Apr 2019 10:32:16 +0300 Subject: [PATCH 1033/1359] fix: emit resume event, silent disconnects, error event param (#3192) * src: Fix shardResumed event not being emitted * docs: Document Client#error again * src: Fix onError due to incorrect typings * src: handle onError properly for both uws and ws * src: Try to fix silent disconnects when using uWs * fix(WebSocketShard): uws emits plain objects, not errors Emitting line of code: https://github.com/discordjs/uws/blob/39aa429f94d9668608f69848b3a84db3a3e92914/src/uws.js#L80-L83 Listener attaching is here: https://github.com/discordjs/uws/blob/master/src/uws.js#L128 For reference, found a clue here: https://github.com/discordjs/discord.js/issues/1528 --- src/client/websocket/WebSocketManager.js | 9 --------- src/client/websocket/WebSocketShard.js | 18 +++++++++++++----- src/client/websocket/handlers/RESUMED.js | 10 +++++----- src/sharding/ShardClientUtil.js | 5 +++++ typings/index.d.ts | 2 +- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index eb4a8282c..71a24757a 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -194,15 +194,6 @@ class WebSocketManager { if (!this.shardQueue.size) this.reconnecting = false; }); - shard.on(ShardEvents.RESUMED, () => { - /** - * Emitted when a shard resumes successfully. - * @event Client#shardResumed - * @param {number} id The shard ID that resumed - */ - this.client.emit(Events.SHARD_RESUMED, shard.id); - }); - shard.on(ShardEvents.CLOSE, event => { if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { /** diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index c927c1561..a317bc042 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -239,7 +239,7 @@ class WebSocketShard extends EventEmitter { /** * Called whenever a message is received. - * @param {Event} event Event received + * @param {MessageEvent} event Event received * @private */ onMessage({ data }) { @@ -266,11 +266,14 @@ class WebSocketShard extends EventEmitter { /** * Called whenever an error occurs with the WebSocket. - * @param {ErrorEvent} error The error that occurred + * @param {ErrorEvent|Object} event The error that occurred * @private */ - onError({ error }) { - if (error && error.message === 'uWs client connection error') { + onError(event) { + const error = event && event.error ? event.error : error; + if (!error) return; + + if (error.message === 'uWs client connection error') { this.debug('Received a uWs error. Closing the connection and reconnecting...'); this.connection.close(4000); return; @@ -295,6 +298,11 @@ class WebSocketShard extends EventEmitter { * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent} */ + /** + * @external MessageEvent + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent} + */ + /** * Called whenever a connection to the gateway is closed. * @param {CloseEvent} event Close event that was received @@ -581,7 +589,7 @@ class WebSocketShard extends EventEmitter { this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); // Close the WebSocket connection, if any - if (this.connection) { + if (this.connection && this.connection.readyState !== WebSocket.CLOSED) { this.connection.close(closeCode); } else { /** diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js index bd00ec883..e345cb74b 100644 --- a/src/client/websocket/handlers/RESUMED.js +++ b/src/client/websocket/handlers/RESUMED.js @@ -5,10 +5,10 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, packet, shard) => { const replayed = shard.sequence - shard.closeSequence; /** - * Emitted when the client gateway resumes. - * @event Client#resume - * @param {number} replayed The number of events that were replayed - * @param {number} shardID The ID of the shard that resumed + * Emitted when a shard resumes successfully. + * @event Client#shardResumed + * @param {number} id The shard ID that resumed + * @param {number} replayedEvents The amount of replayed events */ - client.emit(Events.RESUMED, replayed, shard.id); + client.emit(Events.SHARD_RESUMED, shard.id, replayed); }; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index acbe22508..03c80ead6 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -181,6 +181,11 @@ class ShardClientUtil { _respond(type, message) { this.send(message).catch(err => { err.message = `Error when sending ${type} response to master process: ${err.message}`; + /** + * Emitted when the client encounters an error. + * @event Client#error + * @param {Error} error The error encountered + */ this.client.emit(Events.ERROR, err); }); } diff --git a/typings/index.d.ts b/typings/index.d.ts index d1894df89..f0b064f4b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1339,7 +1339,7 @@ declare module 'discord.js' { private connect(): Promise; private onOpen(): void; private onMessage(event: MessageEvent): void; - private onError(error: ErrorEvent): void; + private onError(error: ErrorEvent | object): void; private onClose(event: CloseEvent): void; private onPacket(packet: object): void; private setHelloTimeout(time?: number): void; From 52b4f09e58f796dd895539784b5f8117fd3df168 Mon Sep 17 00:00:00 2001 From: Dragoteryx <33252782+Dragoteryx@users.noreply.github.com> Date: Sun, 21 Apr 2019 09:34:12 +0200 Subject: [PATCH 1034/1359] fix(Structures): allow multiple extensions by checking prototype chain (#3217) --- src/util/Structures.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Structures.js b/src/util/Structures.js index 5cdfc831a..2dc26770f 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -55,8 +55,8 @@ class Structures { throw new TypeError(`The extender function must return the extended structure class/prototype ${received}.`); } - const prototype = Object.getPrototypeOf(extended); - if (prototype !== structures[structure]) { + if (!(extended.prototype instanceof structures[structure])) { + const prototype = Object.getPrototypeOf(extended); const received = `${extended.name || 'unnamed'}${prototype.name ? ` extends ${prototype.name}` : ''}`; throw new Error( 'The class/prototype returned from the extender function must extend the existing structure class/prototype' + From abd9d368161cbf5c4dd5df79aeeca67ba4d25900 Mon Sep 17 00:00:00 2001 From: Purpzie <25022704+Purpzie@users.noreply.github.com> Date: Sun, 21 Apr 2019 02:38:09 -0500 Subject: [PATCH 1035/1359] feat(Util): resolve text parameter of splitMessage to a string (#3212) --- src/util/Util.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index 700c519bc..828569490 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -51,11 +51,12 @@ class Util { /** * Splits a string into multiple chunks at a designated character that do not exceed a specific length. - * @param {string} text Content to split + * @param {StringResolvable} text Content to split * @param {SplitOptions} [options] Options controlling the behavior of the split * @returns {string|string[]} */ static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { + text = this.resolveString(text); if (text.length <= maxLength) return text; const splitText = text.split(char); if (splitText.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); From 01c708bc759fa85eb5259cb689b95e865ede7e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sun, 21 Apr 2019 13:34:09 +0200 Subject: [PATCH 1036/1359] feat(Sharding): change `waitForReady` to `spawnTimeout` (#3080) This means that you'll not only be able to choose between having a timeout or not, but also to set the amount of milliseconds as you wish. --- src/sharding/Shard.js | 20 +++++++++++--------- src/sharding/ShardClientUtil.js | 7 ++++--- src/sharding/ShardingManager.js | 16 +++++++++------- typings/index.d.ts | 10 +++++----- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index adc5caeb9..4fab578d8 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -102,10 +102,11 @@ class Shard extends EventEmitter { /** * Forks a child process or creates a worker thread for the shard. * You should not need to call this manually. - * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready + * before resolving. (-1 or Infinity for no wait) * @returns {Promise} */ - async spawn(waitForReady = true) { + async spawn(spawnTimeout = 30000) { if (this.process) throw new Error('SHARDING_PROCESS_EXISTS', this.id); if (this.worker) throw new Error('SHARDING_WORKER_EXISTS', this.id); @@ -128,12 +129,12 @@ class Shard extends EventEmitter { */ this.emit('spawn', this.process || this.worker); - if (!waitForReady) return this.process || this.worker; + if (spawnTimeout === -1 || spawnTimeout === Infinity) return this.process || this.worker; await new Promise((resolve, reject) => { this.once('ready', resolve); this.once('disconnect', () => reject(new Error('SHARDING_READY_DISCONNECTED', this.id))); this.once('death', () => reject(new Error('SHARDING_READY_DIED', this.id))); - setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), 30000); + setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), spawnTimeout); }); return this.process || this.worker; } @@ -156,13 +157,14 @@ class Shard extends EventEmitter { /** * Kills and restarts the shard's process/worker. * @param {number} [delay=500] How long to wait between killing the process/worker and restarting it (in milliseconds) - * @param {boolean} [waitForReady=true] Whether to wait until the {@link Client} has become ready before resolving + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready + * before resolving. (-1 or Infinity for no wait) * @returns {Promise} */ - async respawn(delay = 500, waitForReady = true) { + async respawn(delay = 500, spawnTimeout) { this.kill(); if (delay > 0) await Util.delayFor(delay); - return this.spawn(waitForReady); + return this.spawn(spawnTimeout); } /** @@ -308,8 +310,8 @@ class Shard extends EventEmitter { // Shard is requesting a respawn of all shards if (message._sRespawnAll) { - const { shardDelay, respawnDelay, waitForReady } = message._sRespawnAll; - this.manager.respawnAll(shardDelay, respawnDelay, waitForReady).catch(() => { + const { shardDelay, respawnDelay, spawnTimeout } = message._sRespawnAll; + this.manager.respawnAll(shardDelay, respawnDelay, spawnTimeout).catch(() => { // Do nothing }); return; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 03c80ead6..4d0b4a43d 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -143,12 +143,13 @@ class ShardClientUtil { * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds) * @param {number} [respawnDelay=500] How long to wait between killing a shard's process/worker and restarting it * (in milliseconds) - * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait for a shard to become ready before + * continuing to another. (-1 or Infinity for no wait) * @returns {Promise} Resolves upon the message being sent * @see {@link ShardingManager#respawnAll} */ - respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { - return this.send({ _sRespawnAll: { shardDelay, respawnDelay, waitForReady } }); + respawnAll(shardDelay = 5000, respawnDelay = 500, spawnTimeout = 30000) { + return this.send({ _sRespawnAll: { shardDelay, respawnDelay, spawnTimeout } }); } /** diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 80cf71d3f..15b74ffdf 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -156,12 +156,13 @@ class ShardingManager extends EventEmitter { /** * Spawns multiple shards. - * @param {number} [amount=this.totalShards] Number of shards to spawn + * @param {number|string} [amount=this.totalShards] Number of shards to spawn * @param {number} [delay=5500] How long to wait in between spawning each shard (in milliseconds) - * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait until the {@link Client} has become ready + * before resolving. (-1 or Infinity for no wait) * @returns {Promise>} */ - async spawn(amount = this.totalShards, delay = 5500, waitForReady = true) { + async spawn(amount = this.totalShards, delay = 5500, spawnTimeout) { // Obtain/verify the number of shards to spawn if (amount === 'auto') { amount = await Util.fetchRecommendedShards(this.token); @@ -193,7 +194,7 @@ class ShardingManager extends EventEmitter { for (const shardID of this.shardList) { const promises = []; const shard = this.createShard(shardID); - promises.push(shard.spawn(waitForReady)); + promises.push(shard.spawn(spawnTimeout)); if (delay > 0 && this.shards.size !== this.shardList.length) promises.push(Util.delayFor(delay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } @@ -245,13 +246,14 @@ class ShardingManager extends EventEmitter { * @param {number} [shardDelay=5000] How long to wait between shards (in milliseconds) * @param {number} [respawnDelay=500] How long to wait between killing a shard's process and restarting it * (in milliseconds) - * @param {boolean} [waitForReady=true] Whether to wait for a shard to become ready before continuing to another + * @param {number} [spawnTimeout=30000] The amount in milliseconds to wait for a shard to become ready before + * continuing to another. (-1 or Infinity for no wait) * @returns {Promise>} */ - async respawnAll(shardDelay = 5000, respawnDelay = 500, waitForReady = true) { + async respawnAll(shardDelay = 5000, respawnDelay = 500, spawnTimeout) { let s = 0; for (const shard of this.shards.values()) { - const promises = [shard.respawn(respawnDelay, waitForReady)]; + const promises = [shard.respawn(respawnDelay, spawnTimeout)]; if (++s < this.shards.size && shardDelay > 0) promises.push(Util.delayFor(shardDelay)); await Promise.all(promises); // eslint-disable-line no-await-in-loop } diff --git a/typings/index.d.ts b/typings/index.d.ts index f0b064f4b..535295d1f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -921,9 +921,9 @@ declare module 'discord.js' { public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; public kill(): void; - public respawn(delay?: number, waitForReady?: boolean): Promise; + public respawn(delay?: number, spawnTimeout?: number): Promise; public send(message: any): Promise; - public spawn(waitForReady?: boolean): Promise; + public spawn(spawnTimeout?: number): Promise; public on(event: 'death', listener: (child: ChildProcess) => void): this; public on(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; @@ -953,7 +953,7 @@ declare module 'discord.js' { public broadcastEval(script: string): Promise; public broadcastEval(fn: (client: Client) => T): Promise; public fetchClientValues(prop: string): Promise; - public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise; + public respawnAll(shardDelay?: number, respawnDelay?: number, spawnTimeout?: number): Promise; public send(message: any): Promise; public static singleton(client: Client, mode: ShardingManagerMode): ShardClientUtil; @@ -979,8 +979,8 @@ declare module 'discord.js' { public broadcastEval(script: string): Promise; public createShard(id: number): Shard; public fetchClientValues(prop: string): Promise; - public respawnAll(shardDelay?: number, respawnDelay?: number, waitForReady?: boolean): Promise>; - public spawn(amount?: number | 'auto', delay?: number, waitForReady?: boolean): Promise>; + public respawnAll(shardDelay?: number, respawnDelay?: number, spawnTimeout?: number): Promise>; + public spawn(amount?: number | 'auto', delay?: number, spawnTimeout?: number): Promise>; public on(event: 'shardCreate', listener: (shard: Shard) => void): this; From d1778772cdb0935e3eac4479969625c2a010f11c Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Sun, 21 Apr 2019 06:47:09 -0500 Subject: [PATCH 1037/1359] docs: update node version requirement, npm install links, add update guide (#3220) * Update welcome.md Update node version requirement, npm install links, docs links, made a note that the guide is for stable and added a new link to the WIP update guide. * docs(welcome.md): put notice for wip update guild on its own line * docs(welcome.md): indent own line * docs(README.md): apply the same changes here --- README.md | 13 +++++++------ docs/general/welcome.md | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index faaa2bfd1..69a779fc8 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,12 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 8.0.0 or newer is required.** +**Node.js 10.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. -Without voice support: `npm install discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus` -With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript` +Without voice support: `npm install discordjs/discord.js` +With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. @@ -68,8 +68,9 @@ client.login('token'); ## Links * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) -* [Documentation](https://discord.js.org/#/docs) -* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) +* [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) +* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable + See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index eb94f475a..2bc0525c7 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -33,12 +33,12 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 8.0.0 or newer is required.** +**Node.js 10.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. -Without voice support: `npm install discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discord.js node-opus` -With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript` +Without voice support: `npm install discordjs/discord.js` +With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. @@ -74,8 +74,9 @@ client.login('token'); ## Links * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) -* [Documentation](https://discord.js.org/#/docs) -* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) +* [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) +* [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable + See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) From f7f4607b5ffa3fb3a31dac76262f0de7d27def0d Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 22 Apr 2019 08:09:06 +0100 Subject: [PATCH 1038/1359] docs(faq): bump to node 10.0.0 (#3224) --- docs/general/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/general/faq.md b/docs/general/faq.md index 212a16d84..c4ce363d8 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -3,7 +3,7 @@ These questions are some of the most frequently asked. ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ -Update to Node.js 8.0.0 or newer. +Update to Node.js 10.0.0 or newer. ## How do I get voice working? - Install FFMPEG. From c4b79571ba5bd468c6c2b27b7c6f43d421730dd5 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 22 Apr 2019 08:24:32 +0100 Subject: [PATCH 1039/1359] feat(Invite): add deletable getter (#3203) * add Invite#deletable * fix ci * reee * since guild is nullable * accommodate for external invites * nit(Invite): use guild instead of channel.guild --- src/structures/Invite.js | 13 +++++++++++++ typings/index.d.ts | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index fec3f2d7d..43d8c6524 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -1,6 +1,7 @@ 'use strict'; const { Endpoints } = require('../util/Constants'); +const Permissions = require('../util/Permissions'); const Base = require('./Base'); /** @@ -91,6 +92,18 @@ class Invite extends Base { return this.createdTimestamp ? new Date(this.createdTimestamp) : null; } + /** + * Whether the invite is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + const guild = this.guild; + if (!guild || !this.client.guilds.has(guild.id)) return false; + return this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || + guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD); + } + /** * The timestamp the invite will expire at * @type {?number} diff --git a/typings/index.d.ts b/typings/index.d.ts index 535295d1f..f6488599d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -615,7 +615,8 @@ declare module 'discord.js' { constructor(client: Client, data: object); public channel: GuildChannel; public code: string; - public readonly createdAt: Date; + public readonly deletable: boolean; + public readonly createdAt: Date | null; public createdTimestamp: number | null; public readonly expiresAt: Date | null; public readonly expiresTimestamp: number | null; From 23e6414420dccc9d3a87e13d691aaa390eceaa34 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 23 Apr 2019 11:32:03 +0100 Subject: [PATCH 1040/1359] fix: old objects not being cached (#3225) please post issues if you find any, will probably cause some --- src/stores/DataStore.js | 2 +- test/voice.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index ade8c70f7..be1e93d79 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -18,7 +18,7 @@ class DataStore extends Collection { add(data, cache = true, { id, extras = [] } = {}) { const existing = this.get(id || data.id); - if (existing && existing.partial && cache && existing._patch) existing._patch(data); + if (existing && existing._patch && cache) existing._patch(data); if (existing) return existing; const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; diff --git a/test/voice.js b/test/voice.js index 6d022a29c..e8613a58a 100644 --- a/test/voice.js +++ b/test/voice.js @@ -6,7 +6,7 @@ const ytdl = require('ytdl-core'); const prism = require('prism-media'); const fs = require('fs'); -const client = new Discord.Client({ fetchAllMembers: false, partials: true, apiRequestMethod: 'sequential' }); +const client = new Discord.Client({ fetchAllMembers: false, partials: [], apiRequestMethod: 'sequential' }); const auth = require('./auth.js'); From 3f6d08a499e93840054742de28db2783058c7104 Mon Sep 17 00:00:00 2001 From: didinele Date: Tue, 23 Apr 2019 19:47:42 +0300 Subject: [PATCH 1041/1359] fix(typings): Collection#find & findKey can return undefined (#3228) * fix(typings): [Collection#find](https://github.com/didinele/discord.js/blob/master/src/util/Collection.js#L172) can also return undefined * same thing for findKey --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index f6488599d..45f2b582e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -294,8 +294,8 @@ declare module 'discord.js' { public equals(collection: Collection): boolean; public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; - public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V; - public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K; + public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V | undefined; + public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K | undefined; public first(): V | undefined; public first(count: number): V[]; public firstKey(): K | undefined; From 75cd2608082f9f587c9beca84870d5b144c118eb Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 23 Apr 2019 21:44:02 +0300 Subject: [PATCH 1042/1359] src: Don't use the GuildMemberRoleStore to patch the GuildMember _roles (#3226) * src: Don't use the GuildMemberRoleStore for patching GuildMember#_roles * src: Remove usage of _patch from the store * src: Finish up clone changes --- src/stores/GuildMemberRoleStore.js | 10 +++------- src/structures/GuildMember.js | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 7b44a3536..527706299 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -84,7 +84,7 @@ class GuildMemberRoleStore extends Collection { await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason }); const clone = this.member._clone(); - clone.roles._patch([...this.keys(), roleOrRoles.id]); + clone._roles = [...this.keys(), roleOrRoles.id]; return clone; } } @@ -116,7 +116,7 @@ class GuildMemberRoleStore extends Collection { const clone = this.member._clone(); const newRoles = this.filter(role => role.id !== roleOrRoles.id); - clone.roles._patch([...newRoles.keys()]); + clone._roles = [...newRoles.keys()]; return clone; } } @@ -141,13 +141,9 @@ class GuildMemberRoleStore extends Collection { return this.member.edit({ roles }, reason); } - _patch(roles) { - this.member._roles = roles; - } - clone() { const clone = new this.constructor(this.member); - clone._patch(this.keyArray()); + clone.member._roles = [...this.keyArray()]; return clone; } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index e0abea443..567c27e8a 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -76,7 +76,7 @@ class GuildMember extends Base { if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); if (data.user) this.user = this.guild.client.users.add(data.user); - if (data.roles) this.roles._patch(data.roles); + if (data.roles) this._roles = data.roles; } _clone() { From 6a07715c1de19a27dce8c38153ee5ddb06093967 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 23 Apr 2019 21:02:16 +0200 Subject: [PATCH 1043/1359] fix(Guild): only update emojis when they are present in the payload --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9aeb30973..c1d901a47 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -266,7 +266,7 @@ class Guild extends Base { */ this.emojis = new GuildEmojiStore(this); if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji); - } else { + } else if (data.emojis) { this.client.actions.GuildEmojisUpdate.handle({ guild_id: this.id, emojis: data.emojis, From 39115c8acc89ba3997c23e841533f36c5c5d6b7f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 23 Apr 2019 21:21:41 +0200 Subject: [PATCH 1044/1359] fix(MessageCreateAction): remove redundant GuildMemberStore#add call This was also causing a bug where GuildMember#_roles was patched with a GuildMemberRoleStore --- src/client/actions/MessageCreate.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index d4ef64001..3772c4a10 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -12,9 +12,7 @@ class MessageCreateAction extends Action { if (existing) return { message: existing }; const message = channel.messages.add(data); const user = message.author; - let member = null; - if (message.member && channel.guild) member = channel.guild.members.add(message.member); - else if (channel.guild) member = channel.guild.member(user); + let member = message.member; channel.lastMessageID = data.id; if (user) { user.lastMessageID = data.id; From 7b531648e09d9b461cee52897926e11a5debfcea Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 23 Apr 2019 20:59:52 +0100 Subject: [PATCH 1045/1359] feat(GuildMemberStore) add options.count to prune (#3189) * add GuildMemberStore#prune(options.count) * typings: proper typings for null return value --- src/stores/GuildMemberStore.js | 11 ++++++++--- typings/index.d.ts | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 397c119b7..9f9c42117 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -106,11 +106,13 @@ class GuildMemberStore extends DataStore { /** * Prunes members from the guild based on how long they have been inactive. + * It's recommended to set options.count to `false` for large guilds. * @param {Object} [options] Prune options * @param {number} [options.days=7] Number of days of inactivity required to kick * @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them + * @param {boolean} [options.count=true] Whether or not to return the number of users that have been kicked. * @param {string} [options.reason] Reason for this prune - * @returns {Promise} The number of members that were/will be kicked + * @returns {Promise} The number of members that were/will be kicked * @example * // See how many members will be pruned * guild.members.prune({ dry: true }) @@ -122,9 +124,12 @@ class GuildMemberStore extends DataStore { * .then(pruned => console.log(`I just pruned ${pruned} people!`)) * .catch(console.error); */ - prune({ days = 7, dry = false, reason } = {}) { + prune({ days = 7, dry = false, count = true, reason } = {}) { if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE'); - return this.client.api.guilds(this.guild.id).prune[dry ? 'get' : 'post']({ query: { days }, reason }) + return this.client.api.guilds(this.guild.id).prune[dry ? 'get' : 'post']({ query: { + days, + compute_prune_count: count, + }, reason }) .then(data => data.pruned); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 45f2b582e..99b0962e0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1430,6 +1430,7 @@ declare module 'discord.js' { public fetch(options: UserResolvable | FetchMemberOptions): Promise; public fetch(): Promise; public fetch(options: FetchMembersOptions): Promise>; + public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; } @@ -1901,6 +1902,7 @@ declare module 'discord.js' { type GuildResolvable = Guild | Snowflake; interface GuildPruneMembersOptions { + count?: boolean; days?: number; dry?: boolean; reason?: string; From de79bba965c5d5200c1ccdc3b8c7d581b23d4a23 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 24 Apr 2019 17:53:41 +0300 Subject: [PATCH 1046/1359] src: Fix type error in WebSocketShard#onError (#3231) --- src/client/websocket/WebSocketShard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index a317bc042..ca68fa21f 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -270,7 +270,7 @@ class WebSocketShard extends EventEmitter { * @private */ onError(event) { - const error = event && event.error ? event.error : error; + const error = event && event.error ? event.error : event; if (!error) return; if (error.message === 'uWs client connection error') { From 577636a46df5ec4ac2e4fae591604a60fb22f6d2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sat, 27 Apr 2019 10:25:24 +0300 Subject: [PATCH 1047/1359] src: fix random broken reconnects (#3233) --- src/client/websocket/WebSocketShard.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index ca68fa21f..814edb03e 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -311,11 +311,15 @@ class WebSocketShard extends EventEmitter { onClose(event) { this.closeSequence = this.sequence; this.sequence = -1; + this.debug(`WebSocket was closed. Event Code: ${event.code} Clean: ${event.wasClean} Reason: ${event.reason || 'No reason received'}`); + this.setHeartbeatTimer(-1); + this.setHelloTimeout(-1); + this.status = Status.DISCONNECTED; /** From 4d7fc036a1c8e0b80c67242ab6f1eebf69a3fad6 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 27 Apr 2019 11:46:48 +0100 Subject: [PATCH 1048/1359] fix: channels being removed from guild.channels --- src/stores/ChannelStore.js | 7 +++++-- src/stores/GuildChannelStore.js | 9 ++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index aad7927aa..e745eea33 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -54,8 +54,11 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); - if (existing && existing.partial && cache) existing._patch(data); - if (existing) return existing; + if (existing && existing._patch && cache) existing._patch(data); + if (existing) { + guild.channels.add(existing); + return existing; + } const channel = Channel.create(this.client, data, guild); diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 7d95cfe2c..fc150f17b 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -1,6 +1,5 @@ 'use strict'; -const Channel = require('../structures/Channel'); const { ChannelTypes } = require('../util/Constants'); const DataStore = require('./DataStore'); const GuildChannel = require('../structures/GuildChannel'); @@ -16,11 +15,11 @@ class GuildChannelStore extends DataStore { this.guild = guild; } - add(data) { - const existing = this.get(data.id); + add(channel) { + const existing = this.get(channel.id); if (existing) return existing; - - return Channel.create(this.client, data, this.guild); + this.set(channel.id, channel); + return channel; } /** From e0cfb7fb36132abe4367d5ced1650e13a19dbb66 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 27 Apr 2019 13:39:23 +0100 Subject: [PATCH 1049/1359] fix: not checking for guild when creating a channel --- src/stores/ChannelStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index e745eea33..64c482cb3 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -55,7 +55,7 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); if (existing && existing._patch && cache) existing._patch(data); - if (existing) { + if (existing && guild) { guild.channels.add(existing); return existing; } From 4e0cf87d0fd717d781d6e633402fb2b1b6ac6fd3 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 27 Apr 2019 18:52:26 +0100 Subject: [PATCH 1050/1359] fix: typing map being reset for ClientUser (#3216) --- src/structures/ClientUser.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 20bc42b9f..7b54b885e 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -8,6 +8,12 @@ const DataResolver = require('../util/DataResolver'); * @extends {User} */ class ClientUser extends Structures.get('User') { + + constructor(client, data) { + super(client, data); + this._typing = new Map(); + } + _patch(data) { super._patch(data); @@ -23,8 +29,6 @@ class ClientUser extends Structures.get('User') { */ this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; - this._typing = new Map(); - if (data.token) this.client.token = data.token; } From aa253d95518b1c6fad1328372a832702a5ec66f0 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sun, 28 Apr 2019 13:14:26 +0200 Subject: [PATCH 1051/1359] fix(ClientUser): lint --- src/structures/ClientUser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 7b54b885e..16dd13d07 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -8,7 +8,6 @@ const DataResolver = require('../util/DataResolver'); * @extends {User} */ class ClientUser extends Structures.get('User') { - constructor(client, data) { super(client, data); this._typing = new Map(); From bc317466215a222f3b51b9c4082e0dfd6bd071b1 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 29 Apr 2019 19:03:29 +0300 Subject: [PATCH 1052/1359] src: Client#readyAt should be updated when triggerReady is called (#3234) --- src/client/websocket/WebSocketManager.js | 3 +++ src/client/websocket/handlers/READY.js | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 71a24757a..8c2f532f2 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -424,8 +424,11 @@ class WebSocketManager { this.debug('Tried to mark self as ready, but already ready'); return; } + this.status = Status.READY; + this.client.readyAt = new Date(); + /** * Emitted when the client becomes ready to start working. * @event Client#ready diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 74b1e1b9a..89f535f3b 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -6,7 +6,6 @@ module.exports = (client, { d: data }, shard) => { if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); const clientUser = new ClientUser(client, data.user); client.user = clientUser; - client.readyAt = new Date(); client.users.set(clientUser.id, clientUser); for (const guild of data.guilds) { From 23191da13d63ba3a8ceaf7c77f751ed27a22e6f4 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 29 Apr 2019 17:05:52 +0100 Subject: [PATCH 1053/1359] feat(Partials.GuildMember): GuildMemberRemove & Guild#me (#3229) * use partials for GuildMemberRemove & Guild#me * oops * guild.members instead of Action.members Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> --- src/client/actions/Action.js | 9 +++++++++ src/client/actions/GuildMemberRemove.js | 2 +- src/structures/Guild.js | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 0c798089b..5746d2764 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -56,6 +56,15 @@ class GenericAction { } return existing; } + + getMember(data, guild) { + const userID = data.user.id; + const existing = guild.members.get(userID); + if (!existing && this.client.options.partials.includes(PartialTypes.GUILD_MEMBER)) { + return guild.members.add({ user: { id: userID } }); + } + return existing; + } } module.exports = GenericAction; diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 6a4ae397e..108a35da8 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -9,7 +9,7 @@ class GuildMemberRemoveAction extends Action { const guild = client.guilds.get(data.guild_id); let member = null; if (guild) { - member = guild.members.get(data.user.id); + member = this.getMember(data, guild); guild.memberCount--; if (member) { guild.voiceStates.delete(member.id); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index c1d901a47..1b5987f1f 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -393,7 +393,9 @@ class Guild extends Base { * @readonly */ get me() { - return this.members.get(this.client.user.id) || null; + return this.members.get(this.client.user.id) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + this.members.add({ user: { id: this.client.user.id } }, true) : + null); } /** From 870528ed33b22b7a803915965da9f701cf62ccd2 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Mon, 29 Apr 2019 17:35:48 +0100 Subject: [PATCH 1054/1359] feat(VoiceChannel): add editable (#3173) * add VoiceChannel#editable * replace unnecessary super with this Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> --- src/structures/VoiceChannel.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 8b49715fd..860241e9c 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -71,6 +71,15 @@ class VoiceChannel extends GuildChannel { return super.deletable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false); } + /** + * Whether the channel is editable by the client user + * @type {boolean} + * @readonly + */ + get editable() { + return this.manageable && this.permissionsFor(this.client.user).has(Permissions.FLAGS.CONNECT, false); + } + /** * Whether the channel is joinable by the client user * @type {boolean} diff --git a/typings/index.d.ts b/typings/index.d.ts index 99b0962e0..dd117d172 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1148,6 +1148,7 @@ declare module 'discord.js' { constructor(guild: Guild, data?: object); public bitrate: number; public readonly connection: VoiceConnection; + public readonly editable: boolean; public readonly full: boolean; public readonly joinable: boolean; public readonly members: Collection; From 9b0f4b298d12ce95863265c32e8a0308ae51661a Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 29 Apr 2019 11:37:57 -0500 Subject: [PATCH 1055/1359] feature: public raw events (#3159) * add a public alternative to the private raw event while retaining raw for use in debugging privately * only emit dispatch packets * requested changes TIL, that's neat * fix padding * requested changes * Update WebSocketManager.js --- src/client/websocket/WebSocketManager.js | 8 +++++++- src/client/websocket/WebSocketShard.js | 1 + typings/index.d.ts | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 8c2f532f2..23dbadce8 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -1,5 +1,6 @@ 'use strict'; +const EventEmitter = require('events'); const { Error: DJSError } = require('../../errors'); const Collection = require('../../util/Collection'); const Util = require('../../util/Util'); @@ -21,9 +22,14 @@ const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; /** * The WebSocket manager for this client. + * This class forwards raw dispatch events, + * read more about it here {@link https://discordapp.com/developers/docs/topics/gateway} + * @extends EventEmitter */ -class WebSocketManager { +class WebSocketManager extends EventEmitter { constructor(client) { + super(); + /** * The client that instantiated this WebSocketManager * @type {Client} diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 814edb03e..67d494e2c 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -257,6 +257,7 @@ class WebSocketShard extends EventEmitter { try { packet = WebSocket.unpack(this.inflate.result); this.manager.client.emit(Events.RAW, packet, this.id); + if (packet.op === OPCodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id); } catch (err) { this.manager.client.emit(Events.SHARD_ERROR, err, this.id); return; diff --git a/typings/index.d.ts b/typings/index.d.ts index dd117d172..4a57b1ec9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1291,7 +1291,7 @@ declare module 'discord.js' { constructor(id: string, token: string, options?: ClientOptions); } - export class WebSocketManager { + export class WebSocketManager extends EventEmitter { constructor(client: Client); private totalShards: number | string; private shardQueue: Set; @@ -1306,6 +1306,8 @@ declare module 'discord.js' { public status: Status; public readonly ping: number; + public on(event: WSEventType, listener: (data: any, shardID: number) => void): this; + public once(event: WSEventType, listener: (data: any, shardID: number) => void): this; private debug(message: string, shard?: WebSocketShard): void; private connect(): Promise; private createShards(): Promise; From d7a9b7452312a7309d4451dcde99533fec50d766 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 29 Apr 2019 19:49:41 +0300 Subject: [PATCH 1056/1359] src: Replace instanceof Array checks with Array.isArray and instanceof Buffer with Buffer.isBuffer (#3227) * src: Replace instanceof Array checks with Array.isArray * src: Buffer.isBuffer instead of instanceof Buffer --- src/WebSocket.js | 2 +- src/client/Client.js | 8 ++++---- src/client/websocket/WebSocketManager.js | 5 +++-- src/stores/GuildEmojiRoleStore.js | 4 ++-- src/stores/GuildMemberRoleStore.js | 4 ++-- src/structures/APIMessage.js | 8 ++++---- src/structures/Webhook.js | 2 +- src/structures/interfaces/TextBasedChannel.js | 4 ++-- src/util/BitField.js | 6 +++--- src/util/DataResolver.js | 4 ++-- src/util/Util.js | 4 ++-- 11 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index c8f73972a..f3f893b40 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -22,7 +22,7 @@ exports.pack = erlpack ? erlpack.pack : JSON.stringify; exports.unpack = data => { if (!erlpack || data[0] === '{') return JSON.parse(data); - if (!(data instanceof Buffer)) data = Buffer.from(new Uint8Array(data)); + if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data)); return erlpack.unpack(data); }; diff --git a/src/client/Client.js b/src/client/Client.js index bdbc442cc..58d3c7db1 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -49,7 +49,7 @@ class Client extends BaseClient { if (this.options.totalShardCount === DefaultOptions.totalShardCount) { if ('TOTAL_SHARD_COUNT' in data) { this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); - } else if (this.options.shards instanceof Array) { + } else if (Array.isArray(this.options.shards)) { this.options.totalShardCount = this.options.shards.length; } else { this.options.totalShardCount = this.options.shardCount; @@ -365,7 +365,7 @@ class Client extends BaseClient { if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); } - if (options.shards && !(options.shards instanceof Array)) { + if (options.shards && !Array.isArray(options.shards)) { throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); } if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS'); @@ -385,7 +385,7 @@ class Client extends BaseClient { if (typeof options.disableEveryone !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); } - if (!(options.partials instanceof Array)) { + if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); } if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { @@ -394,7 +394,7 @@ class Client extends BaseClient { if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); } - if (!(options.disabledEvents instanceof Array)) { + if (!Array.isArray(options.disabledEvents)) { throw new TypeError('CLIENT_INVALID_OPTION', 'disabledEvents', 'an Array'); } if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 23dbadce8..6dd1a3f22 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -160,8 +160,9 @@ class WebSocketManager extends EventEmitter { } } - if (this.client.options.shards instanceof Array) { - const { shards } = this.client.options; + const { shards } = this.client.options; + + if (Array.isArray(shards)) { this.totalShards = shards.length; this.debug(`Spawning shards: ${shards.join(', ')}`); this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index 2051161d9..ad3793b29 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -33,7 +33,7 @@ class GuildEmojiRoleStore extends Collection { */ add(roleOrRoles) { if (roleOrRoles instanceof Collection) return this.add(roleOrRoles.keyArray()); - if (!(roleOrRoles instanceof Array)) return this.add([roleOrRoles]); + if (!Array.isArray(roleOrRoles)) return this.add([roleOrRoles]); roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { @@ -52,7 +52,7 @@ class GuildEmojiRoleStore extends Collection { */ remove(roleOrRoles) { if (roleOrRoles instanceof Collection) return this.remove(roleOrRoles.keyArray()); - if (!(roleOrRoles instanceof Array)) return this.remove([roleOrRoles]); + if (!Array.isArray(roleOrRoles)) return this.remove([roleOrRoles]); roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolveID(r)); if (roleOrRoles.includes(null)) { diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 527706299..824bca686 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -65,7 +65,7 @@ class GuildMemberRoleStore extends Collection { * @returns {Promise} */ async add(roleOrRoles, reason) { - if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { + if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { throw new TypeError('INVALID_TYPE', 'roles', @@ -96,7 +96,7 @@ class GuildMemberRoleStore extends Collection { * @returns {Promise} */ async remove(roleOrRoles, reason) { - if (roleOrRoles instanceof Collection || roleOrRoles instanceof Array) { + if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) { roleOrRoles = roleOrRoles.map(r => this.guild.roles.resolve(r)); if (roleOrRoles.includes(null)) { throw new TypeError('INVALID_TYPE', 'roles', diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index e6d67b686..a925892d0 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -198,7 +198,7 @@ class APIMessage { split() { if (!this.data) this.resolveData(); - if (!(this.data.content instanceof Array)) return [this]; + if (!Array.isArray(this.data.content)) return [this]; const apiMessages = []; @@ -287,7 +287,7 @@ class APIMessage { * @returns {MessageOptions|WebhookMessageOptions} */ static transformOptions(content, options, extra = {}, isWebhook = false) { - if (!options && typeof content === 'object' && !(content instanceof Array)) { + if (!options && typeof content === 'object' && !Array.isArray(content)) { options = content; content = undefined; } @@ -300,10 +300,10 @@ class APIMessage { return { content, files: [options], ...extra }; } - if (options instanceof Array) { + if (Array.isArray(options)) { const [embeds, files] = this.partitionMessageAdditions(options); return isWebhook ? { content, embeds, files, ...extra } : { content, embed: embeds[0], files, ...extra }; - } else if (content instanceof Array) { + } else if (Array.isArray(content)) { const [embeds, files] = this.partitionMessageAdditions(content); if (embeds.length || files.length) { return isWebhook ? { embeds, files, ...extra } : { embed: embeds[0], files, ...extra }; diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 1ad7cba93..6c1a3db50 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -135,7 +135,7 @@ class Webhook { apiMessage = content.resolveData(); } else { apiMessage = APIMessage.create(this, content, options).resolveData(); - if (apiMessage.data.content instanceof Array) { + if (Array.isArray(apiMessage.data.content)) { return Promise.all(apiMessage.split().map(this.send.bind(this))); } } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 075bc6752..634dff33a 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -138,7 +138,7 @@ class TextBasedChannel { apiMessage = content.resolveData(); } else { apiMessage = APIMessage.create(this, content, options).resolveData(); - if (apiMessage.data.content instanceof Array) { + if (Array.isArray(apiMessage.data.content)) { return Promise.all(apiMessage.split().map(this.send.bind(this))); } } @@ -296,7 +296,7 @@ class TextBasedChannel { * .catch(console.error); */ async bulkDelete(messages, filterOld = false) { - if (messages instanceof Array || messages instanceof Collection) { + if (Array.isArray(messages) || messages instanceof Collection) { let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id || m); if (filterOld) { messageIDs = messageIDs.filter(id => diff --git a/src/util/BitField.js b/src/util/BitField.js index 96b07b829..424a8443d 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -32,7 +32,7 @@ class BitField { * @returns {boolean} */ has(bit) { - if (bit instanceof Array) return bit.every(p => this.has(p)); + if (Array.isArray(bit)) return bit.every(p => this.has(p)); bit = this.constructor.resolve(bit); return (this.bitfield & bit) === bit; } @@ -44,7 +44,7 @@ class BitField { * @returns {string[]} */ missing(bits, ...hasParams) { - if (!(bits instanceof Array)) bits = new this.constructor(bits).toArray(false); + if (!Array.isArray(bits)) bits = new this.constructor(bits).toArray(false); return bits.filter(p => !this.has(p, ...hasParams)); } @@ -136,7 +136,7 @@ class BitField { static resolve(bit = 0) { if (typeof bit === 'number' && bit >= 0) return bit; if (bit instanceof BitField) return bit.bitfield; - if (bit instanceof Array) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); + if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); if (typeof bit === 'string') return this.FLAGS[bit]; throw new RangeError('BITFIELD_INVALID'); } diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 1b9057270..91933eb92 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -62,7 +62,7 @@ class DataResolver { * @returns {?string} */ static resolveBase64(data) { - if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`; + if (Buffer.isBuffer(data)) return `data:image/jpg;base64,${data.toString('base64')}`; return data; } @@ -85,7 +85,7 @@ class DataResolver { * @returns {Promise} */ static resolveFile(resource) { - if (!browser && resource instanceof Buffer) return Promise.resolve(resource); + if (!browser && Buffer.isBuffer(resource)) return Promise.resolve(resource); if (browser && resource instanceof ArrayBuffer) return Promise.resolve(Util.convertToBuffer(resource)); if (typeof resource === 'string') { diff --git a/src/util/Util.js b/src/util/Util.js index 828569490..181d5589e 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -237,7 +237,7 @@ class Util { */ static resolveString(data) { if (typeof data === 'string') return data; - if (data instanceof Array) return data.join('\n'); + if (Array.isArray(data)) return data.join('\n'); return String(data); } @@ -286,7 +286,7 @@ class Util { if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1)); if (color === 'DEFAULT') return 0; color = Colors[color] || parseInt(color.replace('#', ''), 16); - } else if (color instanceof Array) { + } else if (Array.isArray(color)) { color = (color[0] << 16) + (color[1] << 8) + color[2]; } From 2666a9d6dbb6596b5893e7fbdb57c1d907a1ab6b Mon Sep 17 00:00:00 2001 From: bdistin Date: Mon, 29 Apr 2019 11:53:32 -0500 Subject: [PATCH 1057/1359] feat(MessageStore): add remove() (#2468) * MessageStore#remove() * typings --- src/stores/MessageStore.js | 10 ++++++++++ src/structures/Message.js | 12 +++++------- typings/index.d.ts | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index db72114f6..e179daa21 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -82,6 +82,16 @@ class MessageStore extends DataStore { }); } + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user + */ + async remove(message, reason) { + message = this.resolveID(message); + if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); + } + async _fetchId(messageID, cache) { const existing = this.get(messageID); if (existing && !existing.partial) return existing; diff --git a/src/structures/Message.js b/src/structures/Message.js index b57f4299e..c390ac231 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -454,13 +454,11 @@ class Message extends Base { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.client.api.channels(this.channel.id).messages(this.id) - .delete({ reason }) - .then(() => - this.client.actions.MessageDelete.handle({ - id: this.id, - channel_id: this.channel.id, - }).message); + return this.channel.messages.remove(this.id, reason).then(() => + this.client.actions.MessageDelete.handle({ + id: this.id, + channel_id: this.channel.id, + }).message); } else { return new Promise(resolve => { this.client.setTimeout(() => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 4a57b1ec9..1186be6ed 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1448,6 +1448,7 @@ declare module 'discord.js' { public fetch(message: Snowflake, cache?: boolean): Promise; public fetch(options?: ChannelLogsQueryOptions, cache?: boolean): Promise>; public fetchPinned(cache?: boolean): Promise>; + public remove(message: MessageResolvable, reason?: string): Promise; } export class PresenceStore extends DataStore { From ce1e3d20847ea226839a4cba15dcefa875086d12 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:13:41 +0100 Subject: [PATCH 1058/1359] feat(VoiceConnection): add .voice --- src/client/voice/VoiceConnection.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 727f21968..b532d0c5e 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -166,6 +166,14 @@ class VoiceConnection extends EventEmitter { }); } + /** + * The voice state of this connection + * @type {VoiceState} + */ + get voice() { + return this.channel.guild.voiceStates.get(this.client.user.id); + } + /** * Sends a request to the main gateway to join a voice channel. * @param {Object} [options] The options to provide From dd446475371cdbd476e0cb11f79b996d0756e10f Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:24:27 +0100 Subject: [PATCH 1059/1359] voice: Guild.voiceConnection => Guild.voice.connection --- src/client/voice/VoiceConnection.js | 2 +- src/structures/Guild.js | 9 +++++++++ src/structures/VoiceState.js | 11 +++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b532d0c5e..947e19122 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -171,7 +171,7 @@ class VoiceConnection extends EventEmitter { * @type {VoiceState} */ get voice() { - return this.channel.guild.voiceStates.get(this.client.user.id); + return this.channel.guild.voice; } /** diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1b5987f1f..4f5f02c5b 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -398,6 +398,15 @@ class Guild extends Base { null); } + /** + * The voice state for the client user of this guild, if any + * @type {?VoiceState} + * @readonly + */ + get voice() { + return this.me ? this.me.voice : null; + } + /** * Returns the GuildMember form of a User object, if the user is present in the guild. * @param {UserResolvable} user The user that you want to obtain the GuildMember of diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index d74fc6aa0..0b5f2b058 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -1,6 +1,7 @@ 'use strict'; const Base = require('./Base'); +const { browser } = require('../util/Constants'); /** * Represents the voice state for a Guild Member. @@ -77,6 +78,16 @@ class VoiceState extends Base { return this.guild.channels.get(this.channelID) || null; } + /** + * If this is a voice state of the client user, then this will refer to the active VoiceConnection for this guild + * @type {?VoiceConnection} + * @readonly + */ + get connection() { + if (browser || this.id !== this.guild.me.id) return null; + return this.client.voice.connections.get(this.guild.id) || null; + } + /** * Whether this member is either self-deafened or server-deafened * @type {?boolean} From bcb0cd838b9fbcbb7467d4618280a820baf2e0a2 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:29:16 +0100 Subject: [PATCH 1060/1359] voice: remove Guild.voiceConnection and VoiceChannel.connection --- src/client/actions/GuildDelete.js | 2 +- src/client/voice/VoiceBroadcast.js | 2 +- src/structures/Guild.js | 12 +----------- src/structures/VoiceChannel.js | 11 ----------- 4 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 7c61ebacd..193abc47c 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -37,7 +37,7 @@ class GuildDeleteAction extends Action { } for (const channel of guild.channels.values()) this.client.channels.remove(channel.id); - if (guild.voiceConnection) guild.voiceConnection.disconnect(); + if (guild.voice && guild.voice.connection) guild.voice.connection.disconnect(); // Delete guild client.guilds.remove(guild.id); diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index e8751c05e..6b5da074f 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -13,7 +13,7 @@ const PlayInterface = require('./util/PlayInterface'); * const broadcast = client.voice.createBroadcast(); * broadcast.play('./music.mp3'); * // Play "music.mp3" in all voice connections that the client is in - * for (const connection of client.voiceConnections.values()) { + * for (const connection of client.voice.connections.values()) { * connection.play(broadcast); * } * ``` diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 4f5f02c5b..6f86dc14a 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,7 @@ const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); -const { ChannelTypes, DefaultMessageNotifications, PartialTypes, browser } = require('../util/Constants'); +const { ChannelTypes, DefaultMessageNotifications, PartialTypes } = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); @@ -368,16 +368,6 @@ class Guild extends Base { return this.client.channels.get(this.systemChannelID) || null; } - /** - * If the client is connected to any voice channel in this guild, this will be the relevant VoiceConnection - * @type {?VoiceConnection} - * @readonly - */ - get voiceConnection() { - if (browser) return null; - return this.client.voice.connections.get(this.id) || null; - } - /** * The `@everyone` role of the guild * @type {?Role} diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 860241e9c..15f2e496f 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -42,17 +42,6 @@ class VoiceChannel extends GuildChannel { return coll; } - /** - * The voice connection for this voice channel, if the client is connected - * @type {?VoiceConnection} - * @readonly - */ - get connection() { - const connection = this.guild.voiceConnection; - if (connection && connection.channel.id === this.id) return connection; - return null; - } - /** * Checks if the voice channel is full * @type {boolean} From 0d9bc8664dc2dc3b3bf7d35ba7b9050f2797a211 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 29 Apr 2019 19:31:31 +0100 Subject: [PATCH 1061/1359] voice: make Guild.voice more robust --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 6f86dc14a..df18ea347 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -394,7 +394,7 @@ class Guild extends Base { * @readonly */ get voice() { - return this.me ? this.me.voice : null; + return this.voiceStates.get(this.client.user.id); } /** From a59968f7de048118e307faa8e7a5990813b6f338 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Fri, 3 May 2019 18:08:07 +0300 Subject: [PATCH 1062/1359] src: add news and store channels, and missing guild props (#3168) * src: Implement store and news channels! * src: Remove code dupe * src: Add missing guild properties * docs: Add a small notice that the channel type may also change * src: Remove re-creation of the MessageStore * lint: Unused Import * src: Requested changes for StoreChannels * typings: Fix typings * src: Moar guild updates * src: Set maximumPresence to the data prop, the already existent one, or default to 5000 * typings: afkChannel is a VC I keep confusing them, ffs Co-Authored-By: vladfrangu * docs: Document that maximumMembers and maximumPresences may be inaccurate before fetching * src Appels requested changes --- src/client/actions/ChannelUpdate.js | 13 ++- .../websocket/handlers/CHANNEL_UPDATE.js | 2 +- src/structures/Channel.js | 10 ++ src/structures/Guild.js | 97 ++++++++++++++++++- src/structures/NewsChannel.js | 18 ++++ src/structures/StoreChannel.js | 22 +++++ src/structures/TextChannel.js | 1 - src/util/Constants.js | 4 + src/util/Structures.js | 2 + typings/index.d.ts | 34 ++++++- 10 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 src/structures/NewsChannel.js create mode 100644 src/structures/StoreChannel.js diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index b610ea7ca..7b716de07 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -1,14 +1,25 @@ 'use strict'; const Action = require('./Action'); +const Channel = require('../../structures/Channel'); +const { ChannelTypes } = require('../../util/Constants'); class ChannelUpdateAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.id); + let channel = client.channels.get(data.id); if (channel) { const old = channel._update(data); + + if (ChannelTypes[channel.type.toUpperCase()] !== data.type) { + const newChannel = Channel.create(this.client, data, channel.guild); + for (const [id, message] of channel.messages) newChannel.messages.set(id, message); + newChannel._typing = new Map(channel._typing); + channel = newChannel; + this.client.channels.set(channel.id, channel); + } + return { old, updated: channel, diff --git a/src/client/websocket/handlers/CHANNEL_UPDATE.js b/src/client/websocket/handlers/CHANNEL_UPDATE.js index 7a0df486a..01f5beccf 100644 --- a/src/client/websocket/handlers/CHANNEL_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_UPDATE.js @@ -6,7 +6,7 @@ module.exports = (client, packet) => { const { old, updated } = client.actions.ChannelUpdate.handle(packet.d); if (old && updated) { /** - * Emitted whenever a channel is updated - e.g. name change, topic change. + * Emitted whenever a channel is updated - e.g. name change, topic change, channel type change. * @event Client#channelUpdate * @param {DMChannel|GuildChannel} oldChannel The channel before the update * @param {DMChannel|GuildChannel} newChannel The channel after the update diff --git a/src/structures/Channel.js b/src/structures/Channel.js index de5f462e8..ab6adb81a 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -116,6 +116,16 @@ class Channel extends Base { channel = new CategoryChannel(guild, data); break; } + case ChannelTypes.NEWS: { + const NewsChannel = Structures.get('NewsChannel'); + channel = new NewsChannel(guild, data); + break; + } + case ChannelTypes.STORE: { + const StoreChannel = Structures.get('StoreChannel'); + channel = new StoreChannel(guild, data); + break; + } } if (channel) guild.channels.set(channel.id, channel); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index df18ea347..a5d13fa65 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -186,6 +186,27 @@ class Guild extends Base { */ this.embedEnabled = data.embed_enabled; + /** + * Whether widget images are enabled on this guild + * @type {?boolean} + * @name Guild#widgetEnabled + */ + if (typeof data.widget_enabled !== 'undefined') this.widgetEnabled = data.widget_enabled; + + /** + * The widget channel ID, if enabled + * @type {?string} + * @name Guild#widgetChannelID + */ + if (typeof data.widget_channel_id !== 'undefined') this.widgetChannelID = data.widget_channel_id; + + /** + * The embed channel ID, if enabled + * @type {?string} + * @name Guild#embedChannelID + */ + if (typeof data.embed_channel_id !== 'undefined') this.embedChannelID = data.embed_channel_id; + /** * The verification level of the guild * @type {number} @@ -211,12 +232,46 @@ class Guild extends Base { this.joinedTimestamp = data.joined_at ? new Date(data.joined_at).getTime() : this.joinedTimestamp; /** - * The value set for a guild's default message notifications + * The value set for the guild's default message notifications * @type {DefaultMessageNotifications|number} */ this.defaultMessageNotifications = DefaultMessageNotifications[data.default_message_notifications] || data.default_message_notifications; + /** + * The maximum amount of members the guild can have + * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter + * @type {?number} + * @name Guild#maximumMembers + */ + if (typeof data.max_members !== 'undefined') this.maximumMembers = data.max_members || 250000; + + /** + * The maximum amount of presences the guild can have + * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter + * @type {?number} + * @name Guild#maximumPresences + */ + if (typeof data.max_presences !== 'undefined') this.maximumPresences = data.max_presences || 5000; + + /** + * The vanity URL code of the guild, if any + * @type {?string} + */ + this.vanityURLCode = data.vanity_url_code; + + /** + * The description of the guild, if any + * @type {?string} + */ + this.description = data.description; + + /** + * The hash of the guild banner + * @type {?string} + */ + this.banner = data.banner; + this.id = data.id; this.available = !data.unavailable; this.features = data.features || this.features || []; @@ -274,6 +329,16 @@ class Guild extends Base { } } + /** + * The URL to this guild's banner. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + bannerURL({ format, size } = {}) { + if (!this.banner) return null; + return this.client.rest.cdn.Banner(this.id, this.banner, format, size); + } + /** * The timestamp the guild was created at * @type {number} @@ -368,6 +433,24 @@ class Guild extends Base { return this.client.channels.get(this.systemChannelID) || null; } + /** + * Widget channel for this guild + * @type {?TextChannel} + * @readonly + */ + get widgetChannel() { + return this.client.channels.get(this.widgetChannelID) || null; + } + + /** + * Embed channel for this guild + * @type {?TextChannel} + * @readonly + */ + get embedChannel() { + return this.client.channels.get(this.embedChannelID) || null; + } + /** * The `@everyone` role of the guild * @type {?Role} @@ -409,6 +492,17 @@ class Guild extends Base { return this.members.resolve(user); } + /** + * Fetches this guild. + * @returns {Promise} + */ + fetch() { + return this.client.api.guilds(this.id).get().then(data => { + this._patch(data); + return this; + }); + } + /** * An object containing information about a guild member's ban. * @typedef {Object} BanInfo @@ -975,6 +1069,7 @@ class Guild extends Base { }); json.iconURL = this.iconURL(); json.splashURL = this.splashURL(); + json.bannerURL = this.bannerURL(); return json; } diff --git a/src/structures/NewsChannel.js b/src/structures/NewsChannel.js new file mode 100644 index 000000000..76727fcdd --- /dev/null +++ b/src/structures/NewsChannel.js @@ -0,0 +1,18 @@ +'use strict'; + +const TextChannel = require('./TextChannel'); + +/** + * Represents a guild news channel on Discord. + * @extends {TextChannel} + */ +class NewsChannel extends TextChannel { + _patch(data) { + super._patch(data); + + // News channels don't have a rate limit per user, remove it + this.rateLimitPerUser = undefined; + } +} + +module.exports = NewsChannel; diff --git a/src/structures/StoreChannel.js b/src/structures/StoreChannel.js new file mode 100644 index 000000000..87cb04064 --- /dev/null +++ b/src/structures/StoreChannel.js @@ -0,0 +1,22 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); + +/** + * Represents a guild store channel on Discord. + * @extends {GuildChannel} + */ +class StoreChannel extends GuildChannel { + _patch(data) { + super._patch(data); + + /** + * If the guild considers this channel NSFW + * @type {boolean} + * @readonly + */ + this.nsfw = data.nsfw; + } +} + +module.exports = StoreChannel; diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index e50bee05a..836c55a08 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -139,7 +139,6 @@ class TextChannel extends GuildChannel { awaitMessages() {} bulkDelete() {} acknowledge() {} - _cacheMessage() {} } TextBasedChannel.applyToClass(TextChannel, true); diff --git a/src/util/Constants.js b/src/util/Constants.js index f314d3d6f..0d8f3b6e3 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -131,6 +131,8 @@ exports.Endpoints = { if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); }, + Banner: (guildID, hash, format = 'webp', size) => + makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }), Icon: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }), AppIcon: (clientID, hash, { format = 'webp', size } = {}) => @@ -409,6 +411,8 @@ exports.ChannelTypes = { VOICE: 2, GROUP: 3, CATEGORY: 4, + NEWS: 5, + STORE: 6, }; exports.ClientApplicationAssetTypes = { diff --git a/src/util/Structures.js b/src/util/Structures.js index 2dc26770f..02529d26d 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -75,6 +75,8 @@ const structures = { TextChannel: require('../structures/TextChannel'), VoiceChannel: require('../structures/VoiceChannel'), CategoryChannel: require('../structures/CategoryChannel'), + NewsChannel: require('../structures/NewsChannel'), + StoreChannel: require('../structures/StoreChannel'), GuildMember: require('../structures/GuildMember'), Guild: require('../structures/Guild'), Message: require('../structures/Message'), diff --git a/typings/index.d.ts b/typings/index.d.ts index 1186be6ed..ed0430dde 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -122,7 +122,7 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public deleted: boolean; public id: Snowflake; - public type: 'dm' | 'text' | 'voice' | 'category' | 'unknown'; + public type: 'dm' | 'text' | 'voice' | 'category' | 'news' | 'store' | 'unknown'; public delete(reason?: string): Promise; public fetch(): Promise; public toString(): string; @@ -393,9 +393,7 @@ declare module 'discord.js' { private _sortedChannels(channel: Channel): Collection; private _memberSpeakUpdate(user: Snowflake, speaking: boolean): void; - protected setup(data: any): void; - - public readonly afkChannel: VoiceChannel | null; + public readonly afkChannel: VoiceChannel; public afkChannelID: Snowflake; public afkTimeout: number; public applicationID: Snowflake; @@ -432,13 +430,25 @@ declare module 'discord.js' { public readonly systemChannel: TextChannel | null; public systemChannelID: Snowflake; public verificationLevel: number; + public maximumMembers: number; + public maximumPresences: number; + public vanityURLCode: string; + public description: string; + public banner: string; + public widgetEnabled: boolean; + public widgetChannelID: Snowflake; + public readonly widgetChannel: TextChannel; + public embedChannelID: Snowflake; + public readonly embedChannel: TextChannel; public readonly verified: boolean; public readonly voiceConnection: VoiceConnection | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; + public bannerURL(options?: AvatarOptions): string; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; public equals(guild: Guild): boolean; + public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchBans(): Promise>; public fetchIntegrations(): Promise>; @@ -532,6 +542,11 @@ declare module 'discord.js' { public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } + export class StoreChannel extends GuildChannel { + constructor(guild: Guild, data?: object); + public nsfw: boolean; + } + export class GuildEmoji extends Emoji { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; @@ -1064,6 +1079,17 @@ declare module 'discord.js' { public fetchWebhooks(): Promise>; } + export class NewsChannel extends TextBasedChannel(GuildChannel) { + constructor(guild: Guild, data?: object); + public readonly members: Collection; + public messages: MessageStore; + public nsfw: boolean; + public topic: string; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; + public setNSFW(nsfw: boolean, reason?: string): Promise; + public fetchWebhooks(): Promise>; + } + export class User extends PartialTextBasedChannel(Base) { constructor(client: Client, data: object); public avatar: string | null; From 692494dc043e0c98f9e5075138e461c9a972a615 Mon Sep 17 00:00:00 2001 From: Jacz <23615291+MrJacz@users.noreply.github.com> Date: Sat, 4 May 2019 01:11:11 +1000 Subject: [PATCH 1063/1359] feat(VoiceState): self mute/deaf methods (#3243) * Implemented setSelfMute/Deaf, done typings, fixed bug in VoiceState with errors. * Completed requested changes * return send in sendVoiceStateUpdate so its a promise, update typings * Updated methods to return a boolean * Requested changes * Fix bug * Update src/structures/VoiceState.js Co-Authored-By: MrJacz <23615291+MrJacz@users.noreply.github.com> * fix --- src/client/voice/VoiceConnection.js | 7 ++++--- src/errors/Messages.js | 2 ++ src/structures/VoiceState.js | 29 +++++++++++++++++++++++++++++ typings/index.d.ts | 7 +++++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 947e19122..cd2d29a7d 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -177,20 +177,21 @@ class VoiceConnection extends EventEmitter { /** * Sends a request to the main gateway to join a voice channel. * @param {Object} [options] The options to provide + * @returns {Promise} * @private */ sendVoiceStateUpdate(options = {}) { options = Util.mergeDefault({ guild_id: this.channel.guild.id, channel_id: this.channel.id, - self_mute: false, - self_deaf: false, + self_mute: this.voice ? this.voice.selfMute : false, + self_deaf: this.voice ? this.voice.selfDeaf : false, }, options); const queueLength = this.channel.guild.shard.ratelimit.queue.length; this.emit('debug', `Sending voice state update (queue length is ${queueLength}): ${JSON.stringify(options)}`); - this.channel.guild.shard.send({ + return this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, }); diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 1cfe4da9f..1b2bdcdbf 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -51,6 +51,8 @@ const Messages = { VOICE_PRISM_DEMUXERS_NEED_STREAM: 'To play a webm/ogg stream, you need to pass a ReadableStream.', VOICE_STATE_UNCACHED_MEMBER: 'The member of this voice state is uncached.', + VOICE_STATE_NOT_OWN: 'You cannot self-deafen/mute on VoiceStates that do not belong to the ClientUser.', + VOICE_STATE_INVALID_TYPE: name => `${name} must be a boolean.`, UDP_SEND_FAIL: 'Tried to send a UDP packet, but there is no socket available.', UDP_ADDRESS_MALFORMED: 'Malformed UDP address or port.', diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 0b5f2b058..165aa7b66 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -2,6 +2,7 @@ const Base = require('./Base'); const { browser } = require('../util/Constants'); +const { Error, TypeError } = require('../errors'); /** * Represents the voice state for a Guild Member. @@ -138,6 +139,34 @@ class VoiceState extends Base { return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); } + /** + * Self-mutes/unmutes the bot for this voice state. + * @param {boolean} mute Whether or not the bot should be self-muted + * @returns {Promise} true if the voice state was successfully updated, otherwise false + */ + async setSelfMute(mute) { + if (this.id !== this.client.user.id) throw new Error('VOICE_STATE_NOT_OWN'); + if (typeof mute !== 'boolean') throw new TypeError('VOICE_STATE_INVALID_TYPE', 'mute'); + if (!this.connection) return false; + this.selfMute = mute; + await this.connection.sendVoiceStateUpdate(); + return true; + } + + /** + * Self-deafens/undeafens the bot for this voice state. + * @param {boolean} deaf Whether or not the bot should be self-deafened + * @returns {Promise} true if the voice state was successfully updated, otherwise false + */ + async setSelfDeaf(deaf) { + if (this.id !== this.client.user.id) return new Error('VOICE_STATE_NOT_OWN'); + if (typeof deaf !== 'boolean') return new TypeError('VOICE_STATE_INVALID_TYPE', 'deaf'); + if (!this.connection) return false; + this.selfDeaf = deaf; + await this.connection.sendVoiceStateUpdate(); + return true; + } + toJSON() { return super.toJSON({ id: true, diff --git a/typings/index.d.ts b/typings/index.d.ts index ed0430dde..215abc3ae 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1202,7 +1202,7 @@ declare module 'discord.js' { private onSessionDescription(mode: string, secret: string): void; private onSpeaking(data: object): void; private reconnect(token: string, endpoint: string): void; - private sendVoiceStateUpdate(options: object): void; + private sendVoiceStateUpdate(options: object): Promise; private setSessionID(sessionID: string): void; private setSpeaking(value: BitFieldResolvable): void; private setTokenAndEndpoint(token: string, endpoint: string): void; @@ -1215,6 +1215,7 @@ declare module 'discord.js' { public receiver: VoiceReceiver; public speaking: Readonly; public status: VoiceStatus; + public readonly voice: VoiceState; public voiceManager: ClientVoiceManager; public disconnect(): void; public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; @@ -1284,8 +1285,10 @@ declare module 'discord.js' { public sessionID?: string; public readonly speaking: boolean | null; - public setDeaf(mute: boolean, reason?: string): Promise; + public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; + public setSelfDeaf(deaf: boolean): Promise; + public setSelfMute(mute: boolean): Promise; } class VolumeInterface extends EventEmitter { From 176fc47699e43c0eeb5fc9d097ffea692e2cbc75 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 3 May 2019 16:38:57 +0100 Subject: [PATCH 1064/1359] feat(Actions): use partials for messageDeleteBulk (#3240) * make use of partials * don't cache the messages * pass each message within the for..of iteration --- src/client/actions/Action.js | 4 ++-- src/client/actions/MessageDeleteBulk.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 5746d2764..0b03c071b 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -33,14 +33,14 @@ class GenericAction { this.client.channels.get(id)); } - getMessage(data, channel) { + getMessage(data, channel, cache = true) { const id = data.message_id || data.id; return data.message || (this.client.options.partials.includes(PartialTypes.MESSAGE) ? channel.messages.add({ id, channel_id: channel.id, guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), - }) : + }, cache) : channel.messages.get(id)); } diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index 53f4ba051..f80bc7c4d 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -13,7 +13,10 @@ class MessageDeleteBulkAction extends Action { const ids = data.ids; const messages = new Collection(); for (const id of ids) { - const message = channel.messages.get(id); + const message = this.getMessage({ + id, + guild_id: data.guild_id, + }, channel, false); if (message) { message.deleted = true; messages.set(message.id, message); From d7f8fd1ae0a56e3fee7d87c75e4cdfab5f785604 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 4 May 2019 16:21:49 +0100 Subject: [PATCH 1065/1359] fix #3244 --- src/structures/Guild.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index a5d13fa65..b1361aef4 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -57,6 +57,8 @@ class Guild extends Base { */ this.presences = new PresenceStore(this.client); + this.voiceStates = new VoiceStateStore(this); + /** * Whether the bot has been removed from the guild * @type {boolean} @@ -307,8 +309,8 @@ class Guild extends Base { } } - if (!this.voiceStates) this.voiceStates = new VoiceStateStore(this); if (data.voice_states) { + this.voiceStates.clear(); for (const voiceState of data.voice_states) { this.voiceStates.add(voiceState); } From e64773e21bb4070d57ceb1910b30d753b2585c8b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 4 May 2019 16:46:42 +0100 Subject: [PATCH 1066/1359] Add ability to kick members from VoiceChannels and remove duplicated methods (#3242) * feat(voice): kick members from voice channels * fix(VoiceState): improve stability in checking for client user * feat(VoiceState): add setChannel for moving/kicking members * update typings * remove duplicated methods across GuildMember and VoiceState member.setDeaf => member.voice.setDeaf member.setMute => member.voice.setMute member.setVoiceChannel => member.voice.setChannel --- src/structures/GuildMember.js | 36 +++++------------------------------ src/structures/VoiceState.js | 15 ++++++++++++++- typings/index.d.ts | 6 ++---- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 567c27e8a..697f8e061 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -254,7 +254,8 @@ class GuildMember extends Base { * @property {Collection|RoleResolvable[]} [roles] The roles or role IDs to apply * @property {boolean} [mute] Whether or not the member should be muted * @property {boolean} [deaf] Whether or not the member should be deafened - * @property {ChannelResolvable} [channel] Channel to move member to (if they are connected to voice) + * @property {ChannelResolvable|null} [channel] Channel to move member to (if they are connected to voice), or `null` + * if you want to kick them from voice */ /** @@ -270,8 +271,10 @@ class GuildMember extends Base { throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); } data.channel_id = data.channel.id; - data.channel = null; + } else if (data.channel === null) { + data.channel_id = null; } + data.channel = undefined; if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); let endpoint = this.client.api.guilds(this.guild.id); if (this.user.id === this.client.user.id) { @@ -289,35 +292,6 @@ class GuildMember extends Base { return clone; } - /** - * Mutes/unmutes this member. - * @param {boolean} mute Whether or not the member should be muted - * @param {string} [reason] Reason for muting or unmuting - * @returns {Promise} - */ - setMute(mute, reason) { - return this.edit({ mute }, reason); - } - - /** - * Deafens/undeafens this member. - * @param {boolean} deaf Whether or not the member should be deafened - * @param {string} [reason] Reason for deafening or undeafening - * @returns {Promise} - */ - setDeaf(deaf, reason) { - return this.edit({ deaf }, reason); - } - - /** - * Moves this member to the given channel. - * @param {ChannelResolvable} channel The channel to move the member to - * @returns {Promise} - */ - setVoiceChannel(channel) { - return this.edit({ channel }); - } - /** * Sets the nickname for this member. * @param {string} nick The nickname for the guild member diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 165aa7b66..e2cb2ab99 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -85,7 +85,7 @@ class VoiceState extends Base { * @readonly */ get connection() { - if (browser || this.id !== this.guild.me.id) return null; + if (browser || this.id !== this.client.user.id) return null; return this.client.voice.connections.get(this.guild.id) || null; } @@ -139,6 +139,19 @@ class VoiceState extends Base { return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); } + /** + * Moves the member to a different channel, or kick them from the one they're in. + * @param {ChannelResolvable|null} [channel] Channel to move the member to, or `null` if you want to kick them from + * voice + * @param {string} [reason] Reason for moving member to another channel or kicking + * @returns {Promise} + */ + setChannel(channel, reason) { + return this.member ? + this.member.edit({ channel }, reason) : + Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); + } + /** * Self-mutes/unmutes the bot for this voice state. * @param {boolean} mute Whether or not the bot should be self-muted diff --git a/typings/index.d.ts b/typings/index.d.ts index 215abc3ae..726f07428 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -591,10 +591,7 @@ declare module 'discord.js' { public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean; public kick(reason?: string): Promise; public permissionsIn(channel: ChannelResolvable): Readonly; - public setDeaf(deaf: boolean, reason?: string): Promise; - public setMute(mute: boolean, reason?: string): Promise; public setNickname(nickname: string, reason?: string): Promise; - public setVoiceChannel(voiceChannel: ChannelResolvable): Promise; public toJSON(): object; public toString(): string; } @@ -1287,6 +1284,7 @@ declare module 'discord.js' { public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; + public setChannel(channel: ChannelResolvable | null, reason?: string): Promise; public setSelfDeaf(deaf: boolean): Promise; public setSelfMute(mute: boolean): Promise; } @@ -1927,7 +1925,7 @@ declare module 'discord.js' { roles?: Collection | RoleResolvable[]; mute?: boolean; deaf?: boolean; - channel?: ChannelResolvable; + channel?: ChannelResolvable | null; } type GuildMemberResolvable = GuildMember | UserResolvable; From 8b83e2fdcbead62ef018b6423f02e07ade0cd40f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 4 May 2019 19:05:04 +0200 Subject: [PATCH 1067/1359] typings(Presence): add missing guild property --- src/structures/Presence.js | 2 +- typings/index.d.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 55937cea4..22c70e6b3 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -48,7 +48,7 @@ class Presence { * The guild of this presence * @type {?Guild} */ - this.guild = data.guild; + this.guild = data.guild || null; this.patch(data); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 726f07428..35c8a6f1d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -828,11 +828,12 @@ declare module 'discord.js' { export class Presence { constructor(client: Client, data?: object); public activity: Activity | null; - public flags: Readonly; - public status: PresenceStatus; public clientStatus: ClientPresenceStatusData | null; - public readonly user: User | null; + public flags: Readonly; + public guild: Guild | null; public readonly member: GuildMember | null; + public status: PresenceStatus; + public readonly user: User | null; public equals(presence: Presence): boolean; } From 8915bc1d37f1b83fc9bbd3cfef337bd5f75e4779 Mon Sep 17 00:00:00 2001 From: Darqam Date: Mon, 6 May 2019 20:08:56 +0200 Subject: [PATCH 1068/1359] docs:(Client): disambiguate the description of channels collection (#3251) * Disambiguate the description of .channels Although not explicitly said, the current wording makes it seem like all channels are cached and available at any time in this store. Hopefully this variation makes it a bit clearer. * make more explicit (I think) * remove trailing white spaces --- src/client/Client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index 58d3c7db1..257a5cd81 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -113,7 +113,8 @@ class Client extends BaseClient { /** * All of the {@link Channel}s that the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot - * is a member of, and all DM channels + * is a member of. Note that DM channels will not be initially cached, and thus not be present + * in the store without their explicit fetching or use. * @type {ChannelStore} */ this.channels = new ChannelStore(this); From 3d4513268d0aa42a2e822a50ecac47d3996272dd Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Tue, 7 May 2019 08:30:34 -0500 Subject: [PATCH 1069/1359] Add optional zstd for faster WebSocket data inflation (#3223) * zstd --- README.md | 3 +- package.json | 4 +- src/client/websocket/WebSocketShard.js | 60 +++++++++++++++++--------- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 69a779fc8..12ee3a2bd 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ Using opusscript is only recommended for development environments where node-opu For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) +- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) diff --git a/package.json b/package.json index fc5d8d1e0..717fe0656 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "erlpack": "discordapp/erlpack", "libsodium-wrappers": "^0.7.4", "sodium": "^3.0.2", - "zlib-sync": "^0.1.4" + "zlib-sync": "^0.1.4", + "zucc": "^0.1.0" }, "devDependencies": { "@types/node": "^10.12.24", @@ -80,6 +81,7 @@ "sodium": false, "worker_threads": false, "zlib-sync": false, + "zucc": false, "src/sharding/Shard.js": false, "src/sharding/ShardClientUtil.js": false, "src/sharding/ShardingManager.js": false, diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 67d494e2c..861462c5a 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -4,12 +4,21 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); +let zstd; +let decoder; + let zlib; + try { - zlib = require('zlib-sync'); - if (!zlib.Inflate) zlib = require('pako'); -} catch (err) { - zlib = require('pako'); + zstd = require('zucc'); + decoder = new TextDecoder('utf8'); +} catch (e) { + try { + zlib = require('zlib-sync'); + if (!zlib.Inflate) zlib = require('pako'); + } catch (err) { + zlib = require('pako'); + } } /** @@ -206,11 +215,15 @@ class WebSocketShard extends EventEmitter { return; } - this.inflate = new zlib.Inflate({ - chunkSize: 65535, - flush: zlib.Z_SYNC_FLUSH, - to: WebSocket.encoding === 'json' ? 'string' : '', - }); + if (zstd) { + this.inflate = new zstd.DecompressStream(); + } else { + this.inflate = new zlib.Inflate({ + chunkSize: 65535, + flush: zlib.Z_SYNC_FLUSH, + to: WebSocket.encoding === 'json' ? 'string' : '', + }); + } this.debug(`Trying to connect to ${gateway}, version ${client.options.ws.version}`); @@ -219,7 +232,7 @@ class WebSocketShard extends EventEmitter { const ws = this.connection = WebSocket.create(gateway, { v: client.options.ws.version, - compress: 'zlib-stream', + compress: zstd ? 'zstd-stream' : 'zlib-stream', }); ws.onopen = this.onOpen.bind(this); ws.onmessage = this.onMessage.bind(this); @@ -243,19 +256,26 @@ class WebSocketShard extends EventEmitter { * @private */ onMessage({ data }) { - if (data instanceof ArrayBuffer) data = new Uint8Array(data); - const l = data.length; - const flush = l >= 4 && - data[l - 4] === 0x00 && - data[l - 3] === 0x00 && - data[l - 2] === 0xFF && - data[l - 1] === 0xFF; + let raw; + if (zstd) { + const ab = this.inflate.decompress(new Uint8Array(data).buffer); + raw = decoder.decode(ab); + } else { + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + const l = data.length; + const flush = l >= 4 && + data[l - 4] === 0x00 && + data[l - 3] === 0x00 && + data[l - 2] === 0xFF && + data[l - 1] === 0xFF; - this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); - if (!flush) return; + this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); + if (!flush) return; + raw = this.inflate.result; + } let packet; try { - packet = WebSocket.unpack(this.inflate.result); + packet = WebSocket.unpack(raw); this.manager.client.emit(Events.RAW, packet, this.id); if (packet.op === OPCodes.DISPATCH) this.manager.emit(packet.t, packet.d, this.id); } catch (err) { From 0dd3ed72ef3e2c76fe330e13ec572ad05be505a2 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 7 May 2019 20:56:39 +0100 Subject: [PATCH 1070/1359] fix(Partials): Client#event:messageUpdate(oldMessage) and MessageReactionAdd on guild channels (#3250) * ref: add getPayload and use for other get* methods * return existing data.* * use Action.getUser() --- src/client/actions/Action.js | 69 +++++++++++---------- src/client/actions/MessageReactionAdd.js | 2 +- src/client/actions/MessageReactionRemove.js | 2 +- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 0b03c071b..7f21318b4 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -23,47 +23,52 @@ class GenericAction { return data; } - getChannel(data) { - const id = data.channel_id || data.id; - return data.channel || (this.client.options.partials.includes(PartialTypes.CHANNEL) ? - this.client.channels.add({ - id, - guild_id: data.guild_id, - }) : - this.client.channels.get(id)); + getPayload(data, store, id, partialType, cache) { + const existing = store.get(id); + if (!existing && this.client.options.partials.includes(partialType)) { + return store.add(data, cache); + } + return existing; } - getMessage(data, channel, cache = true) { + getChannel(data) { + const id = data.channel_id || data.id; + return data.channel || this.getPayload({ + id, + guild_id: data.guild_id, + }, this.client.channels, id, PartialTypes.CHANNEL); + } + + getMessage(data, channel, cache) { const id = data.message_id || data.id; - return data.message || (this.client.options.partials.includes(PartialTypes.MESSAGE) ? - channel.messages.add({ - id, - channel_id: channel.id, - guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), - }, cache) : - channel.messages.get(id)); + return data.message || this.getPayload({ + id, + channel_id: channel.id, + guild_id: data.guild_id || (channel.guild ? channel.guild.id : null), + }, channel.messages, id, PartialTypes.MESSAGE, cache); } getReaction(data, message, user) { - const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); - const existing = message.reactions.get(emojiID); - if (!existing && this.client.options.partials.includes(PartialTypes.MESSAGE)) { - return message.reactions.add({ - emoji: data.emoji, - count: 0, - me: user.id === this.client.user.id, - }); - } - return existing; + const id = data.emoji.id || decodeURIComponent(data.emoji.name); + return this.getPayload({ + emoji: data.emoji, + count: 0, + me: user.id === this.client.user.id, + }, message.reactions, id, PartialTypes.MESSAGE); } getMember(data, guild) { - const userID = data.user.id; - const existing = guild.members.get(userID); - if (!existing && this.client.options.partials.includes(PartialTypes.GUILD_MEMBER)) { - return guild.members.add({ user: { id: userID } }); - } - return existing; + const id = data.user.id; + return this.getPayload({ + user: { + id, + }, + }, guild.members, id, PartialTypes.GUILD_MEMBER); + } + + getUser(data) { + const id = data.user_id; + return data.user || this.getPayload({ id }, this.client.users, id, PartialTypes.USER); } } diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 185b980ad..e7ae7e26a 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -14,7 +14,7 @@ class MessageReactionAdd extends Action { handle(data) { if (!data.emoji) return false; - const user = data.user || this.client.users.get(data.user_id); + const user = this.getUser(data); if (!user) return false; // Verify channel diff --git a/src/client/actions/MessageReactionRemove.js b/src/client/actions/MessageReactionRemove.js index 4e7995f8e..c6c0d664a 100644 --- a/src/client/actions/MessageReactionRemove.js +++ b/src/client/actions/MessageReactionRemove.js @@ -14,7 +14,7 @@ class MessageReactionRemove extends Action { handle(data) { if (!data.emoji) return false; - const user = this.client.users.get(data.user_id); + const user = this.getUser(data); if (!user) return false; // Verify channel From 75d5598fdada9ad1913b533e70d049de0d4ff7af Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Wed, 8 May 2019 21:03:19 +0100 Subject: [PATCH 1071/1359] import TextDecoder from Util (#3258) --- src/client/websocket/WebSocketShard.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 861462c5a..1ad7065d9 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -3,6 +3,7 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); +const { TextDecoder } = require('util'); let zstd; let decoder; From 72dd872fce70754b0f7616f3d31b5616a48c7188 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 12 May 2019 20:29:28 +0100 Subject: [PATCH 1072/1359] VoiceBroadcast.{dispatchers -> subscribers} --- src/client/voice/VoiceBroadcast.js | 16 ++++++++-------- .../voice/dispatcher/BroadcastDispatcher.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index 6b5da074f..2f30f3550 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -28,10 +28,10 @@ class VoiceBroadcast extends EventEmitter { */ this.client = client; /** - * The dispatchers playing this broadcast + * The subscribed StreamDispatchers of this broadcast * @type {StreamDispatcher[]} */ - this.dispatchers = []; + this.subscribers = []; this.player = new BroadcastAudioPlayer(this); } @@ -66,19 +66,19 @@ class VoiceBroadcast extends EventEmitter { * Ends the broadcast, unsubscribing all subscribed channels and deleting the broadcast */ end() { - for (const dispatcher of this.dispatchers) this.delete(dispatcher); + for (const dispatcher of this.subscribers) this.delete(dispatcher); const index = this.client.voice.broadcasts.indexOf(this); if (index !== -1) this.client.voice.broadcasts.splice(index, 1); } add(dispatcher) { - const index = this.dispatchers.indexOf(dispatcher); + const index = this.subscribers.indexOf(dispatcher); if (index === -1) { - this.dispatchers.push(dispatcher); + this.subscribers.push(dispatcher); /** * Emitted whenever a stream dispatcher subscribes to the broadcast. * @event VoiceBroadcast#subscribe - * @param {StreamDispatcher} dispatcher The subscribed dispatcher + * @param {StreamDispatcher} subscriber The subscribed dispatcher */ this.emit(Events.VOICE_BROADCAST_SUBSCRIBE, dispatcher); return true; @@ -88,9 +88,9 @@ class VoiceBroadcast extends EventEmitter { } delete(dispatcher) { - const index = this.dispatchers.indexOf(dispatcher); + const index = this.subscribers.indexOf(dispatcher); if (index !== -1) { - this.dispatchers.splice(index, 1); + this.subscribers.splice(index, 1); dispatcher.destroy(); /** * Emitted whenever a stream dispatcher unsubscribes to the broadcast. diff --git a/src/client/voice/dispatcher/BroadcastDispatcher.js b/src/client/voice/dispatcher/BroadcastDispatcher.js index d7fd8dfaf..ae8d412ed 100644 --- a/src/client/voice/dispatcher/BroadcastDispatcher.js +++ b/src/client/voice/dispatcher/BroadcastDispatcher.js @@ -15,7 +15,7 @@ class BroadcastDispatcher extends StreamDispatcher { _write(chunk, enc, done) { if (!this.startTime) this.startTime = Date.now(); - for (const dispatcher of this.broadcast.dispatchers) { + for (const dispatcher of this.broadcast.subscribers) { dispatcher._write(chunk, enc); } this._step(done); From 16fcfa3db37ac72d0e0b13a5bc03585903b8905e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 15 May 2019 22:13:12 +0200 Subject: [PATCH 1073/1359] fix(WebSocketManager): rethrow unknown errors when connecting a WebSocketShard --- src/client/websocket/WebSocketManager.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 6dd1a3f22..6dec7c40e 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -264,9 +264,12 @@ class WebSocketManager extends EventEmitter { } catch (error) { if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { throw new DJSError(WSCodes[error.code]); - } else { + // Undefined if session is invalid, error event (or uws' event mimicking it) for regular closes + } else if (!error || error.code) { this.debug('Failed to connect to the gateway, requeueing...', shard); this.shardQueue.add(shard); + } else { + throw error; } } // If we have more shards, add a 5s delay From 55447fd4da08ae9e633dca3c4faff5a9b77d440e Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Wed, 15 May 2019 15:33:27 -0500 Subject: [PATCH 1074/1359] docs(TextChanne): specify unit of rateLimitPerUser (#3272) * Update TextChannel.js Update `setRateLimitPerUser` description to specify the `number` is in seconds, per the Discord docs * Update TextChannel.js Add unit to the rateLimitPerUser property * Update GuildChannel.js --- src/structures/GuildChannel.js | 2 +- src/structures/TextChannel.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 4ba8eb99b..8ffdcfe8f 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -291,7 +291,7 @@ class GuildChannel extends Channel { * Lock the permissions of the channel to what the parent's permissions are * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] * Permission overwrites for the channel - * @property {number} [rateLimitPerUser] The ratelimit per user for the channel + * @property {number} [rateLimitPerUser] The ratelimit per user for the channel in seconds */ /** diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 836c55a08..55c3b0d24 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -50,7 +50,7 @@ class TextChannel extends GuildChannel { this.lastMessageID = data.last_message_id; /** - * The ratelimit per user for this channel + * The ratelimit per user for this channel in seconds * @type {number} */ this.rateLimitPerUser = data.rate_limit_per_user || 0; @@ -66,7 +66,7 @@ class TextChannel extends GuildChannel { /** * Sets the rate limit per user for this channel. - * @param {number} rateLimitPerUser The new ratelimit + * @param {number} rateLimitPerUser The new ratelimit in seconds * @param {string} [reason] Reason for changing the channel's ratelimits * @returns {Promise} */ From 1bafa4b86bcedb62b2fb349dda06f0867273481b Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 16 May 2019 19:56:19 +0200 Subject: [PATCH 1075/1359] fix(READY): do not overwrite Client#user when reidentifying See #3216, this commit attempts to fix losing ClientUser#_typing, which results in no longer being able to clear typing intervals --- src/client/websocket/handlers/READY.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 89f535f3b..039f8f237 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -3,10 +3,14 @@ let ClientUser; module.exports = (client, { d: data }, shard) => { - if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); - const clientUser = new ClientUser(client, data.user); - client.user = clientUser; - client.users.set(clientUser.id, clientUser); + if (client.user) { + client.user._patch(data.user); + } else { + if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); + const clientUser = new ClientUser(client, data.user); + client.user = clientUser; + client.users.set(clientUser.id, clientUser); + } for (const guild of data.guilds) { guild.shardID = shard.id; From 97de79bd5e34ab043a75db1f4fdb738bd20f21c3 Mon Sep 17 00:00:00 2001 From: didinele Date: Thu, 16 May 2019 22:14:46 +0300 Subject: [PATCH 1076/1359] fix(typings): Guild#member can return null (#3274) * fix(typings): Guild#member did not have undefined as a return type * oops, it can apparently return null --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 35c8a6f1d..18575b304 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -459,7 +459,7 @@ declare module 'discord.js' { public fetchEmbed(): Promise; public iconURL(options?: AvatarOptions): string; public leave(): Promise; - public member(user: UserResolvable): GuildMember; + public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; public setChannelPositions(channelPositions: ChannelPosition[]): Promise; From 3ad16fa351cc6a806cd88f1839569b54198664af Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 18 May 2019 14:08:12 +0200 Subject: [PATCH 1077/1359] fix(GuildMember): do not create a channel key when editing This is to not break GuildMember#setNickname for the current user --- src/structures/GuildMember.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 697f8e061..bc2d01e66 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -271,10 +271,11 @@ class GuildMember extends Base { throw new Error('GUILD_VOICE_CHANNEL_RESOLVE'); } data.channel_id = data.channel.id; + data.channel = undefined; } else if (data.channel === null) { data.channel_id = null; + data.channel = undefined; } - data.channel = undefined; if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); let endpoint = this.client.api.guilds(this.guild.id); if (this.user.id === this.client.user.id) { From abebeac193f70e25c842cf4e51e12dd53e1096ca Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 18 May 2019 12:02:23 -0500 Subject: [PATCH 1078/1359] fix(Message#pinnable): you can't pin system messages (#3279) --- src/structures/Message.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index c390ac231..8e5f423ab 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -357,8 +357,8 @@ class Message extends Base { * @readonly */ get pinnable() { - return !this.guild || - this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false); + return this.type === 'DEFAULT' && (!this.guild || + this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false)); } /** From b3060ea229e5a5395f7fb67291ead0440e6c55ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Mon, 20 May 2019 20:49:28 +0200 Subject: [PATCH 1079/1359] typings(Collection): use T in accumulator and initialValue when reducing (#3284) This brings some consistency with Array#reduce's typings and to reality. --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 18575b304..c98e7a16c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -311,7 +311,7 @@ declare module 'discord.js' { public random(count: number): V[]; public randomKey(): K | undefined; public randomKey(count: number): K[]; - public reduce(fn: (accumulator: any, value: V, key: K, collection: Collection) => T, initialValue?: any): T; + public reduce(fn: (accumulator: T, value: V, key: K, collection: Collection) => T, initialValue?: T): T; public some(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; public sort(compareFunction?: (a: V, b: V, c?: K, d?: K) => number): Collection; public sweep(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): number; From 97f3b6c5eb5596501ca4dbbf18b01de4ac5f358e Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 24 May 2019 15:42:09 +0200 Subject: [PATCH 1080/1359] typings(Guild): remove voiceConnection, add voice, cleanup rest Fixes #3293 --- typings/index.d.ts | 48 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c98e7a16c..ee03f4c3b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -398,21 +398,27 @@ declare module 'discord.js' { public afkTimeout: number; public applicationID: Snowflake; public available: boolean; + public banner: string | null; public channels: GuildChannelStore; public readonly createdAt: Date; public readonly createdTimestamp: number; - public readonly defaultRole: Role | null; public defaultMessageNotifications: DefaultMessageNotifications | number; + public readonly defaultRole: Role | null; public deleted: boolean; + public description: string | null; + public embedChannel: GuildChannel | null; + public embedChannelID: Snowflake | null; public embedEnabled: boolean; public emojis: GuildEmojiStore; public explicitContentFilter: number; public features: GuildFeatures[]; - public icon: string; + public icon: string | null; public id: Snowflake; public readonly joinedAt: Date; public joinedTimestamp: number; public large: boolean; + public maximumMembers: number | null; + public maximumPresences: number | null; public readonly me: GuildMember | null; public memberCount: number; public members: GuildMemberStore; @@ -426,24 +432,18 @@ declare module 'discord.js' { public roles: RoleStore; public readonly shard: WebSocketShard; public shardID: number; - public splash: string; + public splash: string | null; public readonly systemChannel: TextChannel | null; - public systemChannelID: Snowflake; + public systemChannelID: Snowflake | null; + public vanityURLCode: string | null; public verificationLevel: number; - public maximumMembers: number; - public maximumPresences: number; - public vanityURLCode: string; - public description: string; - public banner: string; - public widgetEnabled: boolean; - public widgetChannelID: Snowflake; - public readonly widgetChannel: TextChannel; - public embedChannelID: Snowflake; - public readonly embedChannel: TextChannel; public readonly verified: boolean; - public readonly voiceConnection: VoiceConnection | null; + public readonly voice: VoiceState | null; + public readonly widgetChannel: TextChannel | null; + public widgetChannelID: Snowflake | null; + public widgetEnabled: boolean | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; - public bannerURL(options?: AvatarOptions): string; + public bannerURL(options?: AvatarOptions): string | null; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; @@ -451,29 +451,29 @@ declare module 'discord.js' { public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; public fetchBans(): Promise>; + public fetchEmbed(): Promise; public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; - public fetchEmbed(): Promise; - public iconURL(options?: AvatarOptions): string; + public iconURL(options?: AvatarOptions): string | null; public leave(): Promise; public member(user: UserResolvable): GuildMember | null; - public setAFKChannel(afkChannel: ChannelResolvable, reason?: string): Promise; + public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; public setChannelPositions(channelPositions: ChannelPosition[]): Promise; public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications | number, reason?: string): Promise; + public setEmbed(embed: GuildEmbedData, reason?: string): Promise; public setExplicitContentFilter(explicitContentFilter: number, reason?: string): Promise; - public setIcon(icon: Base64Resolvable, reason?: string): Promise; + public setIcon(icon: Base64Resolvable | null, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; public setRegion(region: string, reason?: string): Promise; - public setSplash(splash: Base64Resolvable, reason?: string): Promise; - public setSystemChannel(systemChannel: ChannelResolvable, reason?: string): Promise; + public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; + public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; - public setEmbed(embed: GuildEmbedData, reason?: string): Promise; - public splashURL(options?: AvatarOptions): string; + public splashURL(options?: AvatarOptions): string | null; public toJSON(): object; public toString(): string; } From 34006cb51efd7ea43056c194a65b53e7a080039f Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Fri, 24 May 2019 13:33:40 -0500 Subject: [PATCH 1081/1359] docs(StreamDispatcher): specify pausedTime is in milliseconds (#3295) * Update TextChannel.js Update `setRateLimitPerUser` description to specify the `number` is in seconds, per the Discord docs * Update TextChannel.js Add unit to the rateLimitPerUser property * Update GuildChannel.js * Update StreamDispatcher.js Specify unit for `StreamDispatcher.pausedTime` --- src/client/voice/dispatcher/StreamDispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 340e40c5d..0a3f5220a 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -151,7 +151,7 @@ class StreamDispatcher extends Writable { get paused() { return Boolean(this.pausedSince); } /** - * Total time that this dispatcher has been paused + * Total time that this dispatcher has been paused in milliseconds * @type {number} * @readonly */ From 3f1232ebf3ca9b3a08bee241fc4d2d90b8087d46 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sat, 25 May 2019 12:50:32 +0100 Subject: [PATCH 1082/1359] feat: throw custom error for uncached Guild#me (#3271) * handle cases where Guild#me is uncached * fix id prop * remove unnecessary checks * space's requested changes --- src/errors/Messages.js | 1 + src/structures/GuildAuditLogs.js | 23 ++++++++++++----------- src/structures/GuildEmoji.js | 8 ++++++-- src/structures/GuildMember.js | 1 + src/structures/Invite.js | 1 + 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 1b2bdcdbf..ae42f73c5 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -79,6 +79,7 @@ const Messages = { GUILD_CHANNEL_ORPHAN: 'Could not find a parent to this guild channel.', GUILD_OWNED: 'Guild is owned by the client.', GUILD_MEMBERS_TIMEOUT: 'Members didn\'t arrive in time.', + GUILD_UNCACHED_ME: 'The client user as a member of this guild is uncached.', INVALID_TYPE: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index b466a3aa0..924219194 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -349,19 +349,20 @@ class GuildAuditLogsEntry { guild_id: guild.id, })); } else if (targetType === Targets.INVITE) { - if (guild.me.permissions.has('MANAGE_GUILD')) { - const change = this.changes.find(c => c.key === 'code'); - this.target = guild.fetchInvites() - .then(invites => { + this.target = guild.members.fetch(guild.client.user.id).then(me => { + if (me.permissions.has('MANAGE_GUILD')) { + const change = this.changes.find(c => c.key === 'code'); + return guild.fetchInvites().then(invites => { this.target = invites.find(i => i.code === (change.new || change.old)); - return this.target; }); - } else { - this.target = this.changes.reduce((o, c) => { - o[c.key] = c.new || c.old; - return o; - }, {}); - } + } else { + this.target = this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, {}); + return this.target; + } + }); } else if (targetType === Targets.MESSAGE) { this.target = guild.client.users.get(data.target_id); } else { diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 30c672636..f117a90b4 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -60,6 +60,7 @@ class GuildEmoji extends Emoji { * @readonly */ get deletable() { + if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); return !this.managed && this.guild.me.hasPermission(Permissions.FLAGS.MANAGE_EMOJIS); } @@ -80,8 +81,11 @@ class GuildEmoji extends Emoji { fetchAuthor() { if (this.managed) { return Promise.reject(new Error('EMOJI_MANAGED')); - } else if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { - return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild)); + } else { + if (!this.guild.me) return Promise.reject(new Error('GUILD_UNCACHED_ME')); + if (!this.guild.me.permissions.has(Permissions.FLAGS.MANAGE_EMOJIS)) { + return Promise.reject(new Error('MISSING_MANAGE_EMOJIS_PERMISSION', this.guild)); + } } return this.client.api.guilds(this.guild.id).emojis(this.id).get() .then(emoji => this.client.users.add(emoji.user)); diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index bc2d01e66..6e575d624 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -201,6 +201,7 @@ class GuildMember extends Base { get manageable() { if (this.user.id === this.guild.ownerID) return false; if (this.user.id === this.client.user.id) return false; + if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.guild.me.roles.highest.comparePositionTo(this.roles.highest) > 0; } diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 43d8c6524..b73076a60 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -100,6 +100,7 @@ class Invite extends Base { get deletable() { const guild = this.guild; if (!guild || !this.client.guilds.has(guild.id)) return false; + if (!guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD); } From 9ca36b8eea5906fbfd583810230cd220369b762c Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Sat, 25 May 2019 07:18:44 -0700 Subject: [PATCH 1083/1359] typings(VoiceState): add connection getter (#3292) * fix: add connection to voice state typings * Update typings/index.d.ts Co-Authored-By: SpaceEEC --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index ee03f4c3b..4cd971ea8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1271,6 +1271,7 @@ declare module 'discord.js' { constructor(guild: Guild, data: object); public readonly channel: VoiceChannel | null; public channelID?: Snowflake; + public readonly connection: VoiceConnection | null; public readonly deaf?: boolean; public guild: Guild; public id: Snowflake; From 1ce670daa96f56ac6225e890b38be0470bebe0c1 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sun, 26 May 2019 10:33:58 +0100 Subject: [PATCH 1084/1359] Create FUNDING.yml Just trialling it out --- .github/FUNDING.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..a0273f117 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,11 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL + +github: amishshah +patreon: discordjs From 949488bbbdfe0f6a433e33712ea1cadb4aeab018 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 27 May 2019 14:04:13 +0100 Subject: [PATCH 1085/1359] Fix #3218 --- src/client/voice/VoiceConnection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index cd2d29a7d..b7de316b3 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -433,7 +433,7 @@ class VoiceConnection extends EventEmitter { * @private */ onReady(data) { - this.authentication = data; + Object.assign(this.authentication, data); for (let mode of data.modes) { if (SUPPORTED_MODES.includes(mode)) { this.authentication.mode = mode; From db56e0cbae67f221db9265f6889ae2b4f5c987d4 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 27 May 2019 18:09:54 +0100 Subject: [PATCH 1086/1359] fix: delete VoiceStates even for uncached members --- src/client/actions/GuildMemberRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 108a35da8..5a9700c3f 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -11,8 +11,8 @@ class GuildMemberRemoveAction extends Action { if (guild) { member = this.getMember(data, guild); guild.memberCount--; + guild.voiceStates.delete(data.user.id); if (member) { - guild.voiceStates.delete(member.id); member.deleted = true; guild.members.remove(member.id); /** From 065908956bae6c937de367cddf6ba3a5a092facb Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Mon, 27 May 2019 12:13:25 -0500 Subject: [PATCH 1087/1359] fix websocket unpacking (#3301) --- src/WebSocket.js | 13 ++++++++++++- src/client/websocket/WebSocketShard.js | 7 +------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index f3f893b40..6e6b4e7ed 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -6,9 +6,13 @@ try { if (!erlpack.pack) erlpack = null; } catch (err) {} // eslint-disable-line no-empty +let TextDecoder; + if (browser) { + TextDecoder = window.TextDecoder; // eslint-disable-line no-undef exports.WebSocket = window.WebSocket; // eslint-disable-line no-undef } else { + TextDecoder = require('util').TextDecoder; try { exports.WebSocket = require('@discordjs/uws'); } catch (err) { @@ -16,12 +20,19 @@ if (browser) { } } +const ab = new TextDecoder(); + exports.encoding = erlpack ? 'etf' : 'json'; exports.pack = erlpack ? erlpack.pack : JSON.stringify; exports.unpack = data => { - if (!erlpack || data[0] === '{') return JSON.parse(data); + if (exports.encoding === 'json') { + if (typeof data !== 'string') { + data = ab.decode(data); + } + return JSON.parse(data); + } if (!Buffer.isBuffer(data)) data = Buffer.from(new Uint8Array(data)); return erlpack.unpack(data); }; diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1ad7065d9..3266c5782 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -3,16 +3,12 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); -const { TextDecoder } = require('util'); let zstd; -let decoder; - let zlib; try { zstd = require('zucc'); - decoder = new TextDecoder('utf8'); } catch (e) { try { zlib = require('zlib-sync'); @@ -259,8 +255,7 @@ class WebSocketShard extends EventEmitter { onMessage({ data }) { let raw; if (zstd) { - const ab = this.inflate.decompress(new Uint8Array(data).buffer); - raw = decoder.decode(ab); + raw = this.inflate.decompress(new Uint8Array(data).buffer); } else { if (data instanceof ArrayBuffer) data = new Uint8Array(data); const l = data.length; From d34b62414bca3a73cd5a3df9d2d5b2fcb81557b5 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Mon, 27 May 2019 12:56:40 -0700 Subject: [PATCH 1088/1359] fix: StreamOptions#volume typings (#3303) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4cd971ea8..e3ead1e72 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2183,7 +2183,7 @@ declare module 'discord.js' { interface StreamOptions { type?: StreamType; seek?: number; - volume?: number; + volume?: number | boolean; passes?: number; plp?: number; fec?: boolean; From b5aff6d12006697d77dd10cd49cfca591065d9b3 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 28 May 2019 10:00:57 +0100 Subject: [PATCH 1089/1359] remove member voice state after emitting leave event --- src/client/actions/GuildMemberRemove.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 5a9700c3f..28aa50386 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -11,7 +11,6 @@ class GuildMemberRemoveAction extends Action { if (guild) { member = this.getMember(data, guild); guild.memberCount--; - guild.voiceStates.delete(data.user.id); if (member) { member.deleted = true; guild.members.remove(member.id); @@ -22,6 +21,7 @@ class GuildMemberRemoveAction extends Action { */ if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } + guild.voiceStates.delete(data.user.id); } return { guild, member }; } From 8652e47c14eccd1c8cab27f0d1acb8d7335349e8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Tue, 28 May 2019 14:51:41 +0100 Subject: [PATCH 1090/1359] fix: decode voice ws data as json --- src/WebSocket.js | 4 ++-- src/client/voice/networking/VoiceWebSocket.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebSocket.js b/src/WebSocket.js index 6e6b4e7ed..8c61dfb49 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -26,8 +26,8 @@ exports.encoding = erlpack ? 'etf' : 'json'; exports.pack = erlpack ? erlpack.pack : JSON.stringify; -exports.unpack = data => { - if (exports.encoding === 'json') { +exports.unpack = (data, type) => { + if (exports.encoding === 'json' || type === 'json') { if (typeof data !== 'string') { data = ab.decode(data); } diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 8f1e8340d..6ddabe626 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -136,7 +136,7 @@ class VoiceWebSocket extends EventEmitter { */ onMessage(event) { try { - return this.onPacket(WebSocket.unpack(event.data)); + return this.onPacket(WebSocket.unpack(event.data, 'json')); } catch (error) { return this.onError(error); } From 5154850a54ad4a4d253c14d1ae34db94c68d28fe Mon Sep 17 00:00:00 2001 From: bdistin Date: Thu, 30 May 2019 02:26:49 -0500 Subject: [PATCH 1091/1359] Add Stream permission (#3309) * Add Stream permission * update docs, and DEFAULT Created a new guild to test DEFAULT * update typings --- src/util/Permissions.js | 5 +++-- typings/index.d.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 9f01df399..ef5273ffd 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -41,6 +41,7 @@ class Permissions extends BitField { * * `ADD_REACTIONS` (add new reactions to messages) * * `VIEW_AUDIT_LOG` * * `PRIORITY_SPEAKER` + * * `STREAM` * * `VIEW_CHANNEL` * * `SEND_MESSAGES` * * `SEND_TTS_MESSAGES` @@ -74,7 +75,7 @@ Permissions.FLAGS = { ADD_REACTIONS: 1 << 6, VIEW_AUDIT_LOG: 1 << 7, PRIORITY_SPEAKER: 1 << 8, - + STREAM: 1 << 9, VIEW_CHANNEL: 1 << 10, SEND_MESSAGES: 1 << 11, SEND_TTS_MESSAGES: 1 << 12, @@ -109,6 +110,6 @@ Permissions.ALL = Object.values(Permissions.FLAGS).reduce((all, p) => all | p, 0 * Bitfield representing the default permissions for users * @type {number} */ -Permissions.DEFAULT = 104324097; +Permissions.DEFAULT = 104324673; module.exports = Permissions; diff --git a/typings/index.d.ts b/typings/index.d.ts index e3ead1e72..9fb53f40d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2068,6 +2068,7 @@ declare module 'discord.js' { | 'ADD_REACTIONS' | 'VIEW_AUDIT_LOG' | 'PRIORITY_SPEAKER' + | 'STREAM' | 'VIEW_CHANNEL' | 'SEND_MESSAGES' | 'SEND_TTS_MESSAGES' From 5aa9425040e3057f878f74d892ed9e354543d88f Mon Sep 17 00:00:00 2001 From: DeJay Date: Thu, 30 May 2019 13:57:34 -0500 Subject: [PATCH 1092/1359] Removes the trace packet (#3312) * Removes the trace packet * Update src/client/websocket/WebSocketShard.js Co-Authored-By: Amish Shah * Update src/client/websocket/WebSocketShard.js Co-Authored-By: Amish Shah --- src/client/websocket/WebSocketShard.js | 13 ++----------- typings/index.d.ts | 1 - 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 3266c5782..13ffa9f6d 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -84,13 +84,6 @@ class WebSocketShard extends EventEmitter { */ this.lastHeartbeatAcked = true; - /** - * List of servers the shard is connected to - * @type {string[]} - * @private - */ - this.trace = []; - /** * Contains the rate limit queue and metadata * @type {Object} @@ -368,9 +361,8 @@ class WebSocketShard extends EventEmitter { this.emit(ShardEvents.READY); this.sessionID = packet.d.session_id; - this.trace = packet.d._trace; this.status = Status.READY; - this.debug(`READY ${this.trace.join(' -> ')} | Session ${this.sessionID}.`); + this.debug(`READY | Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); break; @@ -381,10 +373,9 @@ class WebSocketShard extends EventEmitter { */ this.emit(ShardEvents.RESUMED); - this.trace = packet.d._trace; this.status = Status.READY; const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED ${this.trace.join(' -> ')} | Session ${this.sessionID} | Replayed ${replayed} events.`); + this.debug(`RESUMED | Session ${this.sessionID} | Replayed ${replayed} events.`); this.lastHeartbeatAcked = true; this.sendHeartbeat(); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 9fb53f40d..5e077f2b6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1356,7 +1356,6 @@ declare module 'discord.js' { private sessionID?: string; private lastPingTimestamp: number; private lastHeartbeatAcked: boolean; - private trace: string[]; private ratelimit: { queue: object[]; total: number; remaining: number; time: 60e3; timer: NodeJS.Timeout | null; }; private connection: WebSocket | null; private helloTimeout: NodeJS.Timeout | null; From 405bdb5b558eff478d0bde739cee1ee8e40c330b Mon Sep 17 00:00:00 2001 From: "Deivu (Saya)" <36309350+Deivu@users.noreply.github.com> Date: Fri, 31 May 2019 15:06:28 +0800 Subject: [PATCH 1093/1359] cleanup(Constants): remove duplicate VOICE_STATE_UPDATE (#3313) --- src/util/Constants.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 0d8f3b6e3..31f7a8909 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -323,7 +323,6 @@ exports.PartialTypes = keyMirror([ * * USER_UPDATE * * USER_SETTINGS_UPDATE * * PRESENCE_UPDATE - * * VOICE_STATE_UPDATE * * TYPING_START * * VOICE_STATE_UPDATE * * VOICE_SERVER_UPDATE @@ -360,7 +359,6 @@ exports.WSEvents = keyMirror([ 'MESSAGE_REACTION_REMOVE_ALL', 'USER_UPDATE', 'PRESENCE_UPDATE', - 'VOICE_STATE_UPDATE', 'TYPING_START', 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE', From c87758086b81fda632a2f678ed46c4ab2ae8673f Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Wed, 5 Jun 2019 20:34:33 +0100 Subject: [PATCH 1094/1359] feat: add support for premium guilds (#3316) * add premiumTier and premiumSubscriptionCount * add premiumSinceTimestamp and premiumSince * add premium message types * typings * add GuildEmoji#available * fix doc description --- src/structures/Guild.js | 24 ++++++++++++++++++++++++ src/structures/GuildEmoji.js | 7 +++++++ src/structures/GuildMember.js | 17 ++++++++++++++++- src/util/Constants.js | 8 ++++++++ typings/index.d.ts | 13 ++++++++++++- 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index b1361aef4..71eb494e6 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -188,6 +188,30 @@ class Guild extends Base { */ this.embedEnabled = data.embed_enabled; + /** + * The type of premium tier: + * * 0: NONE + * * 1: TIER_1 + * * 2: TIER_2 + * * 3: TIER_3 + * @typedef {number} PremiumTier + */ + + /** + * The premium tier on this guild + * @type {PremiumTier} + */ + this.premiumTier = data.premium_tier; + + /** + * The total number of users currently boosting this server + * @type {?number} + * @name Guild#premiumSubscriptionCount + */ + if (typeof data.premium_subscription_count !== 'undefined') { + this.premiumSubscriptionCount = data.premium_subscription_count; + } + /** * Whether widget images are enabled on this guild * @type {?boolean} diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index f117a90b4..79fff52f7 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -45,6 +45,13 @@ class GuildEmoji extends Emoji { */ if (typeof data.managed !== 'undefined') this.managed = data.managed; + /** + * Whether this emoji is available + * @type {boolean} + * @name GuildEmoji#available + */ + if (typeof data.available !== 'undefined') this.available = data.available; + if (data.roles) this._roles = data.roles; } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 6e575d624..dcd5a93dd 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -39,7 +39,6 @@ class GuildMember extends Base { /** * The timestamp the member joined the guild at * @type {?number} - * @name GuildMember#joinedTimestamp */ this.joinedTimestamp = null; @@ -55,6 +54,12 @@ class GuildMember extends Base { */ this.lastMessageChannelID = null; + /** + * The timestamp of when the member used their Nitro boost on the guild, if it was used + * @type {?number} + */ + this.premiumSinceTimestamp = null; + /** * Whether the member has been removed from the guild * @type {boolean} @@ -74,6 +79,7 @@ class GuildMember extends Base { if (typeof data.nick !== 'undefined') this.nickname = data.nick; if (data.joined_at) this.joinedTimestamp = new Date(data.joined_at).getTime(); + if (data.premium_since) this.premiumSinceTimestamp = new Date(data.premium_since).getTime(); if (data.user) this.user = this.guild.client.users.add(data.user); if (data.roles) this._roles = data.roles; @@ -131,6 +137,15 @@ class GuildMember extends Base { return this.joinedTimestamp ? new Date(this.joinedTimestamp) : null; } + /** + * The time of when the member used their Nitro boost on the guild, if it was used + * @type {?Date} + * @readonly + */ + get premiumSince() { + return this.premiumSinceTimestamp ? new Date(this.premiumSinceTimestamp) : null; + } + /** * The presence of this guild member * @type {Presence} diff --git a/src/util/Constants.js b/src/util/Constants.js index 31f7a8909..a036fb4f0 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -375,6 +375,10 @@ exports.WSEvents = keyMirror([ * * CHANNEL_ICON_CHANGE * * PINS_ADD * * GUILD_MEMBER_JOIN + * * USER_PREMIUM_GUILD_SUBSCRIPTION + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 + * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -386,6 +390,10 @@ exports.MessageTypes = [ 'CHANNEL_ICON_CHANGE', 'PINS_ADD', 'GUILD_MEMBER_JOIN', + 'USER_PREMIUM_GUILD_SUBSCRIPTION', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', + 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', ]; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 5e077f2b6..c56bfb0e7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -427,6 +427,8 @@ declare module 'discord.js' { public readonly nameAcronym: string; public readonly owner: GuildMember | null; public ownerID: Snowflake; + public premiumSubscriptionCount: number | null; + public premiumTier: PremiumTier; public presences: PresenceStore; public region: string; public roles: RoleStore; @@ -551,6 +553,7 @@ declare module 'discord.js' { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; + public available: boolean; public deleted: boolean; public guild: Guild; public managed: boolean; @@ -579,6 +582,8 @@ declare module 'discord.js' { public nickname: string; public readonly partial: boolean; public readonly permissions: Readonly; + public readonly premiumSince: Date | null; + public premiumSinceTimestamp: number | null; public readonly presence: Presence; public roles: GuildMemberRoleStore; public user: User; @@ -2039,7 +2044,11 @@ declare module 'discord.js' { | 'CHANNEL_NAME_CHANGE' | 'CHANNEL_ICON_CHANGE' | 'PINS_ADD' - | 'GUILD_MEMBER_JOIN'; + | 'GUILD_MEMBER_JOIN' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2' + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3'; interface OverwriteData { allow?: PermissionResolvable; @@ -2099,6 +2108,8 @@ declare module 'discord.js' { id: UserResolvable | RoleResolvable; } + type PremiumTier = number; + interface PresenceData { status?: PresenceStatusData; afk?: boolean; From ddcc6cfec9ccb80d42e1d074bf03db2e0dae274e Mon Sep 17 00:00:00 2001 From: Saya <36309350+Deivu@users.noreply.github.com> Date: Thu, 6 Jun 2019 03:37:10 +0800 Subject: [PATCH 1095/1359] docs(Constants): add missing GUILD_EMOJIS_UPDATE to WSEvents (#3325) --- src/util/Constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index a036fb4f0..dc5ddb457 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -309,6 +309,7 @@ exports.PartialTypes = keyMirror([ * * GUILD_ROLE_UPDATE * * GUILD_BAN_ADD * * GUILD_BAN_REMOVE + * * GUILD_EMOJIS_UPDATE * * CHANNEL_CREATE * * CHANNEL_DELETE * * CHANNEL_UPDATE From e87e4a6f0e23aad52744c0254ba224c6518d85a6 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Wed, 5 Jun 2019 15:46:11 -0400 Subject: [PATCH 1096/1359] typings(GuildChannelStore): add CategoryChannel as possible return value (#3326) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c56bfb0e7..7288c015a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1440,7 +1440,7 @@ declare module 'discord.js' { export class GuildChannelStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); - public create(name: string, options?: GuildCreateChannelOptions): Promise; + public create(name: string, options?: GuildCreateChannelOptions): Promise; } // Hacky workaround because changing the signature of an overriden method errors From 8bc8ffe168f9bd37a9fb372bd2b47d8be15d9f40 Mon Sep 17 00:00:00 2001 From: brian <29586909+brxxn@users.noreply.github.com> Date: Wed, 5 Jun 2019 15:57:31 -0400 Subject: [PATCH 1097/1359] feat(Guild): add setRolePositions method(#3317) Allows for role positions to be batch-updated similar to how channel positions are. It uses the Discord API endpoint PATCH /guild/:id/roles --- src/structures/Guild.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 71eb494e6..fc0004875 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -991,6 +991,40 @@ class Guild extends Base { ); } + /** + * The data needed for updating a guild role's position + * @typedef {Object} GuildRolePosition + * @property {GuildRoleResolveable} role The ID of the role + * @property {number} position The position to update + */ + + /** + * Batch-updates the guild's role positions + * @param {GuildRolePosition[]} rolePositions Role positions to update + * @returns {Promise} + * @example + * guild.setRolePositions([{ role: roleID, position: updatedRoleIndex }]) + * .then(guild => console.log(`Role permissions updated for ${guild}`)) + * .catch(console.error); + */ + setRolePositions(rolePositions) { + // Make sure rolePositions are prepared for API + rolePositions = rolePositions.map(o => ({ + id: o.role, + position: o.position, + })); + + // Call the API to update role positions + return this.client.api.guilds(this.id).roles.patch({ + data: rolePositions, + }).then(() => + this.client.actions.GuildRolePositionUpdate.handle({ + guild_id: this.id, + roles: rolePositions, + }).guild + ); + } + /** * Edits the guild's embed. * @param {GuildEmbedData} embed The embed for the guild From 8e1857286d5a9a2033b3ff7eb3bbb68500e46715 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 5 Jun 2019 22:18:01 +0200 Subject: [PATCH 1098/1359] typings(Guild): add typings for setRolePositions See: PR: #3317 Commit: 8bc8ffe168f9bd37a9fb372bd2b47d8be15d9f40 --- src/structures/Guild.js | 2 +- typings/index.d.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index fc0004875..b2adfbc86 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -994,7 +994,7 @@ class Guild extends Base { /** * The data needed for updating a guild role's position * @typedef {Object} GuildRolePosition - * @property {GuildRoleResolveable} role The ID of the role + * @property {RoleResolveable} role The ID of the role * @property {number} position The position to update */ diff --git a/typings/index.d.ts b/typings/index.d.ts index 7288c015a..7dfe5eed3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -472,6 +472,7 @@ declare module 'discord.js' { public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; public setRegion(region: string, reason?: string): Promise; + public setRolePositions(rolePositions: RolePosition[]): Promise; public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; @@ -2176,6 +2177,11 @@ declare module 'discord.js' { mentionable?: boolean; } + interface RolePosition { + role: RoleResolvable; + position: number; + } + type RoleResolvable = Role | string; type ShardingManagerMode = 'process' | 'worker'; From 4a2335c69c2e243f7f39db357acca393273d71ac Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 5 Jun 2019 22:18:06 +0200 Subject: [PATCH 1099/1359] docs(*Resolvable): make them appear on the docs I don't know what part of the docgen is not working properly, but this seems to fix those typedef to not appear on the documentation --- src/stores/ChannelStore.js | 38 ++++++++++++------------- src/stores/GuildChannelStore.js | 50 ++++++++++++++++----------------- src/stores/MessageStore.js | 49 ++++++++++++++++---------------- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 64c482cb3..d0e1caa77 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -78,25 +78,6 @@ class ChannelStore extends DataStore { super.remove(id); } - /** - * Obtains a channel from Discord, or the channel cache if it's already available. - * @param {Snowflake} id ID of the channel - * @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already - * @returns {Promise} - * @example - * // Fetch a channel by its id - * client.channels.fetch('222109930545610754') - * .then(channel => console.log(channel.name)) - * .catch(console.error); - */ - async fetch(id, cache = true) { - const existing = this.get(id); - if (existing && !existing.partial) return existing; - - const data = await this.client.api.channels(id).get(); - return this.add(data, null, cache); - } - /** * Data that can be resolved to give a Channel object. This can be: * * A Channel object @@ -121,6 +102,25 @@ class ChannelStore extends DataStore { * @param {ChannelResolvable} channel The channel resolvable to resolve * @returns {?Snowflake} */ + + /** + * Obtains a channel from Discord, or the channel cache if it's already available. + * @param {Snowflake} id ID of the channel + * @param {boolean} [cache=true] Whether to cache the new channel object if it isn't already + * @returns {Promise} + * @example + * // Fetch a channel by its id + * client.channels.fetch('222109930545610754') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ + async fetch(id, cache = true) { + const existing = this.get(id); + if (existing && !existing.partial) return existing; + + const data = await this.client.api.channels(id).get(); + return this.add(data, null, cache); + } } module.exports = ChannelStore; diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index fc150f17b..55b9ebb0c 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -22,6 +22,31 @@ class GuildChannelStore extends DataStore { return channel; } + /** + * Data that can be resolved to give a Guild Channel object. This can be: + * * A GuildChannel object + * * A Snowflake + * @typedef {GuildChannel|Snowflake} GuildChannelResolvable + */ + + /** + * Resolves a GuildChannelResolvable to a Channel object. + * @method resolve + * @memberof GuildChannelStore + * @instance + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?Channel} + */ + + /** + * Resolves a GuildChannelResolvable to a channel ID string. + * @method resolveID + * @memberof GuildChannelStore + * @instance + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?Snowflake} + */ + /** * Creates a new channel in the guild. * @param {string} name The name of the new channel @@ -90,31 +115,6 @@ class GuildChannelStore extends DataStore { }); return this.client.actions.ChannelCreate.handle(data).channel; } - - /** - * Data that can be resolved to give a Guild Channel object. This can be: - * * A GuildChannel object - * * A Snowflake - * @typedef {GuildChannel|Snowflake} GuildChannelResolvable - */ - - /** - * Resolves a GuildChannelResolvable to a Channel object. - * @method resolve - * @memberof GuildChannelStore - * @instance - * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve - * @returns {?Channel} - */ - - /** - * Resolves a GuildChannelResolvable to a channel ID string. - * @method resolveID - * @memberof GuildChannelStore - * @instance - * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve - * @returns {?Snowflake} - */ } module.exports = GuildChannelStore; diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index e179daa21..59b224f72 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -82,31 +82,6 @@ class MessageStore extends DataStore { }); } - /** - * Deletes a message, even if it's not cached. - * @param {MessageResolvable} message The message to delete - * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user - */ - async remove(message, reason) { - message = this.resolveID(message); - if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); - } - - async _fetchId(messageID, cache) { - const existing = this.get(messageID); - if (existing && !existing.partial) return existing; - const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); - return this.add(data, cache); - } - - async _fetchMany(options = {}, cache) { - const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message, cache)); - return messages; - } - - /** * Data that can be resolved to a Message object. This can be: * * A Message @@ -131,6 +106,30 @@ class MessageStore extends DataStore { * @param {MessageResolvable} message The message resolvable to resolve * @returns {?Snowflake} */ + + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user + */ + async remove(message, reason) { + message = this.resolveID(message); + if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); + } + + async _fetchId(messageID, cache) { + const existing = this.get(messageID); + if (existing && !existing.partial) return existing; + const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); + return this.add(data, cache); + } + + async _fetchMany(options = {}, cache) { + const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this.add(message, cache)); + return messages; + } } module.exports = MessageStore; From 19ef45130b8d3a226500a0ccd69f7d90aaa02430 Mon Sep 17 00:00:00 2001 From: Alex <41408947+S0ftwareUpd8@users.noreply.github.com> Date: Sat, 8 Jun 2019 01:38:45 -0700 Subject: [PATCH 1100/1359] docs(Guild): add missing features (#3336) The addition of missing guild features that were added in the Nitro boost update, such as ANIMATED_ICON --- src/structures/Guild.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index b2adfbc86..820fac513 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -144,6 +144,12 @@ class Guild extends Base { /** * An array of enabled guild features, here are the possible values: + * * ANIMATED_ICON + * * COMMERCE + * * LURKABLE + * * PARTNERED + * * NEWS + * * BANNER * * INVITE_SPLASH * * MORE_EMOJI * * VERIFIED From 6cd4c27faed8b3f1b9da41a87b7922cd286313b9 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sat, 8 Jun 2019 16:18:50 +0200 Subject: [PATCH 1101/1359] docs(Client): fetchVoiceRegions returns a promise --- src/client/Client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Client.js b/src/client/Client.js index 257a5cd81..85a93bb8a 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -256,7 +256,7 @@ class Client extends BaseClient { /** * Obtains the available voice regions from Discord. - * @returns {Collection} + * @returns {Promise>} * @example * client.fetchVoiceRegions() * .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`)) From f82f0af9286b4ebf1350c725ba4251fef89b8b83 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Jun 2019 05:54:12 +1000 Subject: [PATCH 1102/1359] docs(Presence): document client property (#3342) --- src/structures/Presence.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 22c70e6b3..8c7d22b0d 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -37,6 +37,12 @@ class Presence { * @param {Object} [data={}] The data for the presence */ constructor(client, data = {}) { + /** + * The client that instantiated this + * @name Presence#client + * @type {Client} + * @readonly + */ Object.defineProperty(this, 'client', { value: client }); /** * The user ID of this presence From 1bec28bd8105aa09f43492491be9dd9cc7f7d701 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Thu, 13 Jun 2019 14:14:47 +0200 Subject: [PATCH 1103/1359] feat(Guild): default iconURL to gif if animated (#3338) * feat(Guild): default iconURL to gif if animated * Icon, not Banner * fix url --- src/util/Constants.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index dc5ddb457..993563c07 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -133,8 +133,10 @@ exports.Endpoints = { }, Banner: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }), - Icon: (guildID, hash, format = 'webp', size) => - makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }), + Icon: (guildID, hash, format = 'default', size) => { + if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; + return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }); + }, AppIcon: (clientID, hash, { format = 'webp', size } = {}) => makeImageUrl(`${root}/app-icons/${clientID}/${hash}`, { size, format }), AppAsset: (clientID, hash, { format = 'webp', size } = {}) => From 6100aceef24abfff0d32fcf5a6ff79c2102d046f Mon Sep 17 00:00:00 2001 From: anandre <38661761+anandre@users.noreply.github.com> Date: Wed, 19 Jun 2019 11:49:33 -0500 Subject: [PATCH 1104/1359] docs(RoleStore): update -> create in create method (#3349) Under `create`, change `update` -> `create` in the description --- src/stores/RoleStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 6f97d2770..db3e2c0be 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -76,7 +76,7 @@ class RoleStore extends DataStore { * Creates a new role in the guild with given information. * The position will silently reset to 1 if an invalid one is provided, or none. * @param {Object} [options] Options - * @param {RoleData} [options.data] The data to update the role with + * @param {RoleData} [options.data] The data to create the role with * @param {string} [options.reason] Reason for creating this role * @returns {Promise} * @example From a22aabf6a827f8643cf84939e9065b6c37b289e0 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 25 Jun 2019 13:31:48 -0500 Subject: [PATCH 1105/1359] feature: teams support (#3350) * basic teams support * export Team & TeamMember * use typedef * typings and some fixes * Update src/structures/TeamMember.js Co-Authored-By: Vlad Frangu * fix Team#iconURL() * fix typings and a bug * fix states start at 1 * team icon hash can be null * fix owner typings --- src/index.js | 2 + src/structures/ClientApplication.js | 5 +- src/structures/Team.js | 109 ++++++++++++++++++++++++++++ src/structures/TeamMember.js | 62 ++++++++++++++++ src/util/Constants.js | 15 ++++ typings/index.d.ts | 33 ++++++++- 6 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/structures/Team.js create mode 100644 src/structures/TeamMember.js diff --git a/src/index.js b/src/index.js index b3a92f60c..916056fe0 100644 --- a/src/index.js +++ b/src/index.js @@ -85,6 +85,8 @@ module.exports = { ReactionEmoji: require('./structures/ReactionEmoji'), RichPresenceAssets: require('./structures/Presence').RichPresenceAssets, Role: require('./structures/Role'), + Team: require('./structures/Team'), + TeamMember: require('./structures/TeamMember'), TextChannel: require('./structures/TextChannel'), User: require('./structures/User'), VoiceChannel: require('./structures/VoiceChannel'), diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index d2bcd17a7..ac712bb6a 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -3,6 +3,7 @@ const Snowflake = require('../util/Snowflake'); const { ClientApplicationAssetTypes, Endpoints } = require('../util/Constants'); const Base = require('./Base'); +const Team = require('./Team'); const AssetTypes = Object.keys(ClientApplicationAssetTypes); @@ -67,9 +68,9 @@ class ClientApplication extends Base { /** * The owner of this OAuth application - * @type {?User} + * @type {User|Team} */ - this.owner = data.owner ? this.client.users.add(data.owner) : null; + this.owner = data.team ? new Team(this.client, data.team) : this.client.users.add(data.owner); } /** diff --git a/src/structures/Team.js b/src/structures/Team.js new file mode 100644 index 000000000..0832a31f3 --- /dev/null +++ b/src/structures/Team.js @@ -0,0 +1,109 @@ +'use strict'; + +const Snowflake = require('../util/Snowflake'); +const Collection = require('../util/Collection'); +const Base = require('./Base'); +const TeamMember = require('./TeamMember'); + +/** + * Represents a Client OAuth2 Application Team. + * @extends {Base} + */ +class Team extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + /** + * The ID of the Team + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The name of the Team + * @type {string} + */ + this.name = data.name; + + /** + * The Team's icon hash + * @type {?string} + */ + this.icon = data.icon || null; + + /** + * The Team's owner id + * @type {?string} + */ + this.ownerID = data.owner_user_id || null; + + /** + * The Team's members + * @type {Collection} + */ + this.members = new Collection(); + + for (const memberData of data.members) { + const member = new TeamMember(this.client, this, memberData); + this.members.set(member.id, member); + } + } + + /** + * The owner of this team + * @type {?TeamMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerID) || null; + } + + /** + * The timestamp the app was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the app was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the application's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} URL to the icon + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.TeamIcon(this.id, this.icon, { format, size }); + } + + /** + * When concatenated with a string, this automatically returns the Team's name instead of the + * Team object. + * @returns {string} + * @example + * // Logs: Team name: My Team + * console.log(`Team name: ${team}`); + */ + toString() { + return this.name; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } +} + +module.exports = Team; diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js new file mode 100644 index 000000000..f37fcbe04 --- /dev/null +++ b/src/structures/TeamMember.js @@ -0,0 +1,62 @@ +'use strict'; + +const Base = require('./Base'); +const { MembershipStates } = require('../util/Constants'); + +/** + * Represents a Client OAuth2 Application Team Member. + * @extends {Base} + */ +class TeamMember extends Base { + constructor(client, team, data) { + super(client); + + /** + * The Team this member is part of + * @type {Team} + */ + this.team = team; + + this._patch(data); + } + + _patch(data) { + /** + * The permissions this Team Member has with reguard to the team + * @type {string[]} + */ + this.permissions = data.permissions; + + /** + * The permissions this Team Member has with reguard to the team + * @type {MembershipStates} + */ + this.membershipState = MembershipStates[data.membership_state]; + + /** + * The user for this Team Member + * @type {User} + */ + this.user = this.client.users.add(data.user); + + /** + * The ID of the Team Member + * @type {Snowflake} + */ + this.id = this.user.id; + } + + /** + * When concatenated with a string, this automatically returns the team members's tag instead of the + * TeamMember object. + * @returns {string} + * @example + * // Logs: Team Member's tag: @Hydrabolt + * console.log(`Team Member's tag: ${teamMember}`); + */ + toString() { + return this.user.toString(); + } +} + +module.exports = TeamMember; diff --git a/src/util/Constants.js b/src/util/Constants.js index 993563c07..0587163cd 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -145,6 +145,8 @@ exports.Endpoints = { makeImageUrl(`${root}/channel-icons/${channelID}/${hash}`, { size, format }), Splash: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/splashes/${guildID}/${hash}`, { size, format }), + TeamIcon: (teamID, hash, { format = 'webp', size } = {}) => + makeImageUrl(`${root}/team-icons/${teamID}/${hash}`, { size, format }), }; }, invite: (root, code) => `${root}/${code}`, @@ -569,6 +571,19 @@ exports.DefaultMessageNotifications = [ 'MENTIONS', ]; +/** + * The value set for a team members's membership state: + * * INVITED + * * ACCEPTED + * @typedef {string} MembershipStates + */ +exports.MembershipStates = [ + // They start at 1 + null, + 'INVITED', + 'ACCEPTED', +]; + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/typings/index.d.ts b/typings/index.d.ts index 7dfe5eed3..558e12eb9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -255,7 +255,7 @@ declare module 'discord.js' { public icon: string; public id: Snowflake; public name: string; - public owner: User | null; + public owner: User | Team; public rpcOrigins: string[]; public coverImage(options?: AvatarOptions): string; public fetchAssets(): Promise; @@ -264,6 +264,34 @@ declare module 'discord.js' { public toString(): string; } + export class Team extends Base { + constructor(client: Client, data: object); + public id: Snowflake; + public name: string; + public icon: string | null; + public ownerID: Snowflake | null; + public members: Collection; + + public readonly owner: TeamMember; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + + public iconURL(options?: AvatarOptions): string; + public toJSON(): object; + public toString(): string; + } + + export class TeamMember extends Base { + constructor(client: Client, team: Team, data: object); + public team: Team; + public id: Snowflake; + public permissions: string[]; + public membershipState: MembershipStates; + public user: User; + + public toString(): string; + } + export interface ActivityOptions { name?: string; url?: string; @@ -1992,6 +2020,9 @@ declare module 'discord.js' { type InviteResolvable = string; + type MembershipStates = 'INVITED' + | 'ACCEPTED'; + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; From d7b2146c81d401d0fdefd8ec040a54dfce6067e5 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 25 Jun 2019 20:40:15 +0200 Subject: [PATCH 1106/1359] refactor(TeamMember): make id a getter --- src/structures/TeamMember.js | 13 ++++++++----- typings/index.d.ts | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js index f37fcbe04..e5567143c 100644 --- a/src/structures/TeamMember.js +++ b/src/structures/TeamMember.js @@ -38,12 +38,15 @@ class TeamMember extends Base { * @type {User} */ this.user = this.client.users.add(data.user); + } - /** - * The ID of the Team Member - * @type {Snowflake} - */ - this.id = this.user.id; + /** + * The ID of the Team Member + * @type {Snowflake} + * @readonly + */ + get id() { + return this.user.id; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 558e12eb9..8d4c02943 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -284,7 +284,7 @@ declare module 'discord.js' { export class TeamMember extends Base { constructor(client: Client, team: Team, data: object); public team: Team; - public id: Snowflake; + public readonly id: Snowflake; public permissions: string[]; public membershipState: MembershipStates; public user: User; From 61e2e3e1add12d4ad6db63d3199387e8fc026d3e Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 26 Jun 2019 12:36:12 +0300 Subject: [PATCH 1107/1359] docs: add zucc to docsite welcome page (#3355) --- docs/general/welcome.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 2bc0525c7..da1e02e89 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -47,6 +47,7 @@ For production bots, using node-opus should be considered a necessity, especiall ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`) +- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) From 4c11347511e24021ec4765c44ffb3cdf53e53619 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 26 Jun 2019 11:42:03 +0200 Subject: [PATCH 1108/1359] docs(Team*): fix appliction -> team, tag -> mention --- src/structures/Team.js | 6 +++--- src/structures/TeamMember.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/structures/Team.js b/src/structures/Team.js index 0832a31f3..cec984b6b 100644 --- a/src/structures/Team.js +++ b/src/structures/Team.js @@ -62,7 +62,7 @@ class Team extends Base { } /** - * The timestamp the app was created at + * The timestamp the team was created at * @type {number} * @readonly */ @@ -71,7 +71,7 @@ class Team extends Base { } /** - * The time the app was created at + * The time the team was created at * @type {Date} * @readonly */ @@ -80,7 +80,7 @@ class Team extends Base { } /** - * A link to the application's icon. + * A link to the teams's icon. * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} URL to the icon */ diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js index e5567143c..03bcf6ec2 100644 --- a/src/structures/TeamMember.js +++ b/src/structures/TeamMember.js @@ -22,13 +22,13 @@ class TeamMember extends Base { _patch(data) { /** - * The permissions this Team Member has with reguard to the team + * The permissions this Team Member has with regard to the team * @type {string[]} */ this.permissions = data.permissions; /** - * The permissions this Team Member has with reguard to the team + * The permissions this Team Member has with regard to the team * @type {MembershipStates} */ this.membershipState = MembershipStates[data.membership_state]; @@ -50,12 +50,12 @@ class TeamMember extends Base { } /** - * When concatenated with a string, this automatically returns the team members's tag instead of the + * When concatenated with a string, this automatically returns the team members's mention instead of the * TeamMember object. * @returns {string} * @example - * // Logs: Team Member's tag: @Hydrabolt - * console.log(`Team Member's tag: ${teamMember}`); + * // Logs: Team Member's mention: <@123456789012345678> + * console.log(`Team Member's mention: ${teamMember}`); */ toString() { return this.user.toString(); From 1dd4c041e08068a8979a14cbdc5be95233634f2d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 26 Jun 2019 20:12:05 +0200 Subject: [PATCH 1109/1359] fix(ClientApplication): owner is still nullable Fixes #3358 --- src/structures/ClientApplication.js | 8 ++++++-- typings/index.d.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index ac712bb6a..9288d900f 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -68,9 +68,13 @@ class ClientApplication extends Base { /** * The owner of this OAuth application - * @type {User|Team} + * @type {?User|Team} */ - this.owner = data.team ? new Team(this.client, data.team) : this.client.users.add(data.owner); + this.owner = data.team ? + new Team(this.client, data.team) ? + data.owner : + this.client.users.add(data.owner) : + null; } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 8d4c02943..de3255919 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -255,7 +255,7 @@ declare module 'discord.js' { public icon: string; public id: Snowflake; public name: string; - public owner: User | Team; + public owner: User | Team | null; public rpcOrigins: string[]; public coverImage(options?: AvatarOptions): string; public fetchAssets(): Promise; From b65a4f05da40a45e33f78fc43c9b8172a170a3df Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 26 Jun 2019 20:19:05 +0200 Subject: [PATCH 1110/1359] fix(ClientApplication): fix ternaries --- src/structures/ClientApplication.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 9288d900f..395d1db59 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -71,10 +71,10 @@ class ClientApplication extends Base { * @type {?User|Team} */ this.owner = data.team ? - new Team(this.client, data.team) ? - data.owner : + new Team(this.client, data.team) : + data.owner ? this.client.users.add(data.owner) : - null; + null; } /** From 20d7b3de5905bb0317483436df979f4250c67c1f Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Thu, 4 Jul 2019 10:20:28 -0400 Subject: [PATCH 1111/1359] docs/typings(VoiceStateStore): document and type the class (#3294) * Update index.d.ts * Update Guild.js * Update Guild.js * docs/typings(VoiceStateStore): document and add typings --- src/stores/VoiceStateStore.js | 4 ++++ src/structures/Guild.js | 4 ++++ typings/index.d.ts | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js index ece3c2bb6..a5eaac2dc 100644 --- a/src/stores/VoiceStateStore.js +++ b/src/stores/VoiceStateStore.js @@ -3,6 +3,10 @@ const DataStore = require('./DataStore'); const VoiceState = require('../structures/VoiceState'); +/** + * Stores voice states. + * @extends {DataStore} + */ class VoiceStateStore extends DataStore { constructor(guild, iterable) { super(guild.client, iterable, VoiceState); diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 820fac513..30f3175f9 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -57,6 +57,10 @@ class Guild extends Base { */ this.presences = new PresenceStore(this.client); + /** + * A collection of voice states in this guild + * @type {VoiceStateStore} + */ this.voiceStates = new VoiceStateStore(this); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index de3255919..df4831d00 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -469,6 +469,7 @@ declare module 'discord.js' { public verificationLevel: number; public readonly verified: boolean; public readonly voice: VoiceState | null; + public readonly voiceStates: VoiceStateStore; public readonly widgetChannel: TextChannel | null; public widgetChannelID: Snowflake | null; public widgetEnabled: boolean | null; @@ -1542,6 +1543,10 @@ declare module 'discord.js' { public fetch(id: Snowflake, cache?: boolean): Promise; } + export class VoiceStateStore extends DataStore { + constructor(guild: Guild, iterable?: Iterable); + } + //#endregion //#region Mixins From 4069d009d1a99393ec53a0e8bbcbbefa2c7765d2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 11 Jul 2019 13:59:19 +0300 Subject: [PATCH 1112/1359] misc: automatically add labels to issues that use a template (#3367) * misc: Automatically add labels to issues that use the template * misc: Ditto * misc: DITTO --- .github/ISSUE_TEMPLATE/bug_report.md | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 1 + .github/ISSUE_TEMPLATE/question---general-support-request.md | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d78410ccb..2cfa22985 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,7 @@ --- name: Bug report about: Report incorrect or unexpected behaviour of discord.js +labels: s: unverified, type: bug --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ee9d888af..45ef822bf 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,7 @@ --- name: Feature request about: Request a feature for the core discord.js library +labels: type: enhancement --- diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md index 8ec717c90..aa3153ea2 100644 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ b/.github/ISSUE_TEMPLATE/question---general-support-request.md @@ -1,6 +1,7 @@ --- name: Question / General support request about: Ask for help in Discord instead - https://discord.gg/bRCvFy9 +labels: question (please use Discord instead) --- From adb082305d41194f0392353f1da29d43f2bc5617 Mon Sep 17 00:00:00 2001 From: Jisagi Date: Thu, 11 Jul 2019 13:09:43 +0200 Subject: [PATCH 1113/1359] feat(Guild): add banner to edit method and add setBanner method (#3364) * add setBanner method to Guild * typos fixed & typings added * more typings * docs(Guild): add banner to GuildEditData --- src/structures/Guild.js | 16 ++++++++++++++++ typings/index.d.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 30f3175f9..90728501f 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -770,6 +770,7 @@ class Guild extends Base { * @property {Base64Resolvable} [icon] The icon of the guild * @property {GuildMemberResolvable} [owner] The owner of the guild * @property {Base64Resolvable} [splash] The splash screen of the guild + * @property {Base64Resolvable} [banner] The banner of the guild * @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications */ @@ -802,6 +803,7 @@ class Guild extends Base { if (typeof data.icon !== 'undefined') _data.icon = data.icon; if (data.owner) _data.owner_id = this.client.users.resolve(data.owner).id; if (data.splash) _data.splash = data.splash; + if (data.banner) _data.banner = data.banner; if (typeof data.explicitContentFilter !== 'undefined') { _data.explicit_content_filter = Number(data.explicitContentFilter); } @@ -971,6 +973,20 @@ class Guild extends Base { return this.edit({ splash: await DataResolver.resolveImage(splash), reason }); } + /** + * Sets a new guild banner. + * @param {Base64Resolvable|BufferResolvable} banner The new banner of the guild + * @param {string} [reason] Reason for changing the guild's banner + * @returns {Promise} + * @example + * guild.setBanner('./banner.png') + * .then(updated => console.log('Updated the guild banner')) + * .catch(console.error); + */ + async setBanner(banner, reason) { + return this.edit({ banner: await DataResolver.resolveImage(banner), reason }); + } + /** * The data needed for updating a channel's position. * @typedef {Object} ChannelPosition diff --git a/typings/index.d.ts b/typings/index.d.ts index df4831d00..42e8d2dc0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -493,6 +493,7 @@ declare module 'discord.js' { public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; public setAFKTimeout(afkTimeout: number, reason?: string): Promise; + public setBanner(banner: Base64Resolvable | null, reason?: string): Promise; public setChannelPositions(channelPositions: ChannelPosition[]): Promise; public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications | number, reason?: string): Promise; public setEmbed(embed: GuildEmbedData, reason?: string): Promise; @@ -1947,6 +1948,7 @@ declare module 'discord.js' { icon?: Base64Resolvable; owner?: GuildMemberResolvable; splash?: Base64Resolvable; + banner?: Base64Resolvable; } interface GuildEmbedData { From f1433a2d97fb037660a33dc577d005d7f9676dda Mon Sep 17 00:00:00 2001 From: Eduardo Londero Date: Thu, 11 Jul 2019 16:40:12 -0300 Subject: [PATCH 1114/1359] feat(Collector): add idle time for a Collector to stop itself (#2942) * Implement idle feature * Add typings * Minimal fixes * Make everything in Collector and not attached to ReactionCollector * set this._idletimeout to null when collector ends * also set this._timeout to null when collector ends --- src/structures/interfaces/Collector.js | 23 ++++++++++++++++++++++- typings/index.d.ts | 2 ++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 8e6d8463f..cadb44c65 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -16,6 +16,7 @@ const EventEmitter = require('events'); * Options to be applied to the collector. * @typedef {Object} CollectorOptions * @property {number} [time] How long to run the collector for in milliseconds + * @property {number} [idle] How long to stop the collector after inactivity in milliseconds * @property {boolean} [dispose=false] Whether to dispose data when it's deleted */ @@ -66,10 +67,18 @@ class Collector extends EventEmitter { */ this._timeout = null; + /** + * Timeout for cleanup due to inactivity + * @type {?Timeout} + * @private + */ + this._idletimeout = null; + this.handleCollect = this.handleCollect.bind(this); this.handleDispose = this.handleDispose.bind(this); if (options.time) this._timeout = this.client.setTimeout(() => this.stop('time'), options.time); + if (options.idle) this._idletimeout = this.client.setTimeout(() => this.stop('idle'), options.idle); } /** @@ -89,6 +98,11 @@ class Collector extends EventEmitter { * @param {...*} args The arguments emitted by the listener */ this.emit('collect', ...args); + + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), this.options.idle); + } } this.checkEnd(); } @@ -155,7 +169,14 @@ class Collector extends EventEmitter { stop(reason = 'user') { if (this.ended) return; - if (this._timeout) this.client.clearTimeout(this._timeout); + if (this._timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = null; + } + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = null; + } this.ended = true; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 42e8d2dc0..b97c62a82 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -350,6 +350,7 @@ declare module 'discord.js' { export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); private _timeout: NodeJS.Timer | null; + private _idletimeout: NodeJS.Timer | null; public readonly client: Client; public collected: Collection; @@ -1759,6 +1760,7 @@ declare module 'discord.js' { interface CollectorOptions { time?: number; + idle?: number; dispose?: boolean; } From 00c4098bb315a92322d708fe4cf71d070508ab69 Mon Sep 17 00:00:00 2001 From: bdistin Date: Thu, 11 Jul 2019 15:08:40 -0500 Subject: [PATCH 1115/1359] refactor(Util.escapeMarkdown): allow separate escaping and add tests (#3241) * wip refactor * add escapeMarkdown tests * italics can be done with a single underscore too * more refined * fix test name * unnecessary eslint ignores * use jest * make eslint less annoying in this test file * more testing * fix lib usage * more tests and a small fix --- package.json | 1 + src/structures/APIMessage.js | 2 +- src/util/Util.js | 151 ++++++++++++++++++++++++++-- test/escapeMarkdown.test.js | 184 +++++++++++++++++++++++++++++++++++ typings/index.d.ts | 10 +- 5 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 test/escapeMarkdown.test.js diff --git a/package.json b/package.json index 717fe0656..6c454decf 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@types/ws": "^6.0.1", "discord.js-docgen": "discordjs/docgen", "eslint": "^5.13.0", + "jest": "^24.7.1", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", "tslint": "^5.12.1", diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index a925892d0..6edd65785 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -93,7 +93,7 @@ class APIMessage { if (content || mentionPart) { if (isCode) { const codeName = typeof this.options.code === 'string' ? this.options.code : ''; - content = `${mentionPart}\`\`\`${codeName}\n${Util.escapeMarkdown(content || '', true)}\n\`\`\``; + content = `${mentionPart}\`\`\`${codeName}\n${Util.cleanCodeBlockContent(content || '')}\n\`\`\``; if (isSplit) { splitOptions.prepend = `${splitOptions.prepend || ''}\`\`\`${codeName}\n`; splitOptions.append = `\n\`\`\`${splitOptions.append || ''}`; diff --git a/src/util/Util.js b/src/util/Util.js index 181d5589e..2f12d5870 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -75,14 +75,144 @@ class Util { /** * Escapes any Discord-flavour markdown in a string. * @param {string} text Content to escape - * @param {boolean} [onlyCodeBlock=false] Whether to only escape codeblocks (takes priority) - * @param {boolean} [onlyInlineCode=false] Whether to only escape inline code + * @param {Object} [options={}] What types of markdown to escape + * @param {boolean} [options.codeBlock=true] Whether to escape code blocks or not + * @param {boolean} [options.inlineCode=true] Whether to escape inline code or not + * @param {boolean} [options.bold=true] Whether to escape bolds or not + * @param {boolean} [options.italic=true] Whether to escape italics or not + * @param {boolean} [options.underline=true] Whether to escape underlines or not + * @param {boolean} [options.strikethrough=true] Whether to escape strikethroughs or not + * @param {boolean} [options.spoiler=true] Whether to escape spoilers or not + * @param {boolean} [options.codeBlockContent=true] Whether to escape text inside code blocks or not + * @param {boolean} [options.inlineCodeContent=true] Whether to escape text inside inline code or not * @returns {string} */ - static escapeMarkdown(text, onlyCodeBlock = false, onlyInlineCode = false) { - if (onlyCodeBlock) return text.replace(/```/g, '`\u200b``'); - if (onlyInlineCode) return text.replace(/\\(`|\\)/g, '$1').replace(/(`|\\)/g, '\\$1'); - return text.replace(/\\(\*|_|`|~|\\)/g, '$1').replace(/(\*|_|`|~|\\)/g, '\\$1'); + static escapeMarkdown(text, { + codeBlock = true, + inlineCode = true, + bold = true, + italic = true, + underline = true, + strikethrough = true, + spoiler = true, + codeBlockContent = true, + inlineCodeContent = true, + } = {}) { + if (!codeBlockContent) { + return text.split('```').map((subString, index, array) => { + if ((index % 2) && index !== array.length - 1) return subString; + return Util.escapeMarkdown(subString, { + inlineCode, + bold, + italic, + underline, + strikethrough, + spoiler, + inlineCodeContent, + }); + }).join(codeBlock ? '\\`\\`\\`' : '```'); + } + if (!inlineCodeContent) { + return text.split(/(?<=^|[^`])`(?=[^`]|$)/g).map((subString, index, array) => { + if ((index % 2) && index !== array.length - 1) return subString; + return Util.escapeMarkdown(subString, { + codeBlock, + bold, + italic, + underline, + strikethrough, + spoiler, + }); + }).join(inlineCode ? '\\`' : '`'); + } + if (inlineCode) text = Util.escapeInlineCode(text); + if (codeBlock) text = Util.escapeCodeBlock(text); + if (italic) text = Util.escapeItalic(text); + if (bold) text = Util.escapeBold(text); + if (underline) text = Util.escapeUnderline(text); + if (strikethrough) text = Util.escapeStrikethrough(text); + if (spoiler) text = Util.escapeSpoiler(text); + return text; + } + + /** + * Escapes code block markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeCodeBlock(text) { + return text.replace(/```/g, '\\`\\`\\`'); + } + + /** + * Escapes inline code markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeInlineCode(text) { + return text.replace(/(?<=^|[^`])`(?=[^`]|$)/g, '\\`'); + } + + /** + * Escapes italic markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeItalic(text) { + let i = 0; + text = text.replace(/(?<=^|[^*])\*([^*]|\*\*|$)/g, (_, match) => { + if (match === '**') return ++i % 2 ? `\\*${match}` : `${match}\\*`; + return `\\*${match}`; + }); + i = 0; + return text.replace(/(?<=^|[^_])_([^_]|__|$)/g, (_, match) => { + if (match === '__') return ++i % 2 ? `\\_${match}` : `${match}\\_`; + return `\\_${match}`; + }); + } + + /** + * Escapes bold markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeBold(text) { + let i = 0; + return text.replace(/\*\*(\*)?/g, (_, match) => { + if (match) return ++i % 2 ? `${match}\\*\\*` : `\\*\\*${match}`; + return '\\*\\*'; + }); + } + + /** + * Escapes underline markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeUnderline(text) { + let i = 0; + return text.replace(/__(_)?/g, (_, match) => { + if (match) return ++i % 2 ? `${match}\\_\\_` : `\\_\\_${match}`; + return '\\_\\_'; + }); + } + + /** + * Escapes strikethrough markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeStrikethrough(text) { + return text.replace(/~~/g, '\\~\\~'); + } + + /** + * Escapes spoiler markdown in a string. + * @param {string} text Content to escape + * @returns {string} + */ + static escapeSpoiler(text) { + return text.replace(/\|\|/g, '\\|\\|'); } /** @@ -421,6 +551,15 @@ class Util { }); } + /** + * The content to put in a codeblock with all codeblock fences replaced by the equivalent backticks. + * @param {string} text The string to be converted + * @returns {string} + */ + static cleanCodeBlockContent(text) { + return text.replace('```', '`\u200b``'); + } + /** * Creates a Promise that resolves after a specified duration. * @param {number} ms How long to wait before resolving (in milliseconds) diff --git a/test/escapeMarkdown.test.js b/test/escapeMarkdown.test.js new file mode 100644 index 000000000..b791fdb91 --- /dev/null +++ b/test/escapeMarkdown.test.js @@ -0,0 +1,184 @@ +'use strict'; + +/* eslint-disable max-len, no-undef */ + +const Util = require('../src/util/Util'); +const testString = '`_Behold!_`\n||___~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~___||'; + +describe('escapeCodeblock', () => { + test('shared', () => { + expect(Util.escapeCodeBlock(testString)) + .toBe('`_Behold!_`\n||___~~***\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`***~~___||'); + }); + + test('basic', () => { + expect(Util.escapeCodeBlock('```test```')) + .toBe('\\`\\`\\`test\\`\\`\\`'); + }); +}); + + +describe('escapeInlineCode', () => { + test('shared', () => { + expect(Util.escapeInlineCode(testString)) + .toBe('\\`_Behold!_\\`\n||___~~***```js\n\\`use strict\\`;\nrequire(\'discord.js\');```***~~___||'); + }); + + test('basic', () => { + expect(Util.escapeInlineCode('`test`')) + .toBe('\\`test\\`'); + }); +}); + + +describe('escapeBold', () => { + test('shared', () => { + expect(Util.escapeBold(testString)) + .toBe('`_Behold!_`\n||___~~*\\*\\*```js\n`use strict`;\nrequire(\'discord.js\');```\\*\\**~~___||'); + }); + + test('basic', () => { + expect(Util.escapeBold('**test**')) + .toBe('\\*\\*test\\*\\*'); + }); +}); + + +describe('escapeItalic', () => { + test('shared', () => { + expect(Util.escapeItalic(testString)) + .toBe('`\\_Behold!\\_`\n||\\___~~\\***```js\n`use strict`;\nrequire(\'discord.js\');```**\\*~~__\\_||'); + }); + + test('basic (_)', () => { + expect(Util.escapeItalic('_test_')) + .toBe('\\_test\\_'); + }); + + test('basic (*)', () => { + expect(Util.escapeItalic('*test*')) + .toBe('\\*test\\*'); + }); +}); + + +describe('escapeUnderline', () => { + test('shared', () => { + expect(Util.escapeUnderline(testString)) + .toBe('`_Behold!_`\n||_\\_\\_~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~\\_\\__||'); + }); + + test('basic', () => { + expect(Util.escapeUnderline('__test__')) + .toBe('\\_\\_test\\_\\_'); + }); +}); + + +describe('escapeStrikethrough', () => { + test('shared', () => { + expect(Util.escapeStrikethrough(testString)) + .toBe('`_Behold!_`\n||___\\~\\~***```js\n`use strict`;\nrequire(\'discord.js\');```***\\~\\~___||'); + }); + + test('basic', () => { + expect(Util.escapeStrikethrough('~~test~~')) + .toBe('\\~\\~test\\~\\~'); + }); +}); + + +describe('escapeSpoiler', () => { + test('shared', () => { + expect(Util.escapeSpoiler(testString)) + .toBe('`_Behold!_`\n\\|\\|___~~***```js\n`use strict`;\nrequire(\'discord.js\');```***~~___\\|\\|'); + }); + + test('basic', () => { + expect(Util.escapeSpoiler('||test||')) + .toBe('\\|\\|test\\|\\|'); + }); +}); + + +describe('escapeMarkdown', () => { + test('shared', () => { + expect(Util.escapeMarkdown(testString)) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no codeBlock', () => { + expect(Util.escapeMarkdown(testString, { codeBlock: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n\\`use strict\\`;\nrequire(\'discord.js\');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no inlineCode', () => { + expect(Util.escapeMarkdown(testString, { inlineCode: false })) + .toBe('`\\_Behold!\\_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no bold', () => { + expect(Util.escapeMarkdown(testString, { bold: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\***\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`**\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no italic', () => { + expect(Util.escapeMarkdown(testString, { italic: false })) + .toBe('\\`_Behold!_\\`\n\\|\\|_\\_\\_\\~\\~*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\**\\~\\~\\_\\__\\|\\|'); + }); + + test('no underline', () => { + expect(Util.escapeMarkdown(testString, { underline: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\___\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~__\\_\\|\\|'); + }); + + test('no strikethrough', () => { + expect(Util.escapeMarkdown(testString, { strikethrough: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_~~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*~~\\_\\_\\_\\|\\|'); + }); + + test('no spoiler', () => { + expect(Util.escapeMarkdown(testString, { spoiler: false })) + .toBe('\\`\\_Behold!\\_\\`\n||\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_||'); + }); + + describe('code content', () => { + test('no code block content', () => { + expect(Util.escapeMarkdown(testString, { codeBlockContent: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('no inline code content', () => { + expect(Util.escapeMarkdown(testString, { inlineCodeContent: false })) + .toBe('\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n\\`use strict\\`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('neither inline code or code block content', () => { + expect(Util.escapeMarkdown(testString, { inlineCodeContent: false, codeBlockContent: false })) + // eslint-disable-next-line max-len + .toBe('\\`_Behold!_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('neither code blocks or code block content', () => { + expect(Util.escapeMarkdown(testString, { codeBlock: false, codeBlockContent: false })) + .toBe('\\`\\_Behold!\\_\\`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*```js\n`use strict`;\nrequire(\'discord.js\');```\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('neither inline code or inline code content', () => { + expect(Util.escapeMarkdown(testString, { inlineCode: false, inlineCodeContent: false })) + .toBe('`_Behold!_`\n\\|\\|\\_\\_\\_\\~\\~\\*\\*\\*\\`\\`\\`js\n`use strict`;\nrequire(\'discord.js\');\\`\\`\\`\\*\\*\\*\\~\\~\\_\\_\\_\\|\\|'); + }); + + test('edge-case odd number of fenses with no code block content', () => { + expect(Util.escapeMarkdown('**foo** ```**bar**``` **fizz** ``` **buzz**', { codeBlock: false, codeBlockContent: false })) + .toBe('\\*\\*foo\\*\\* ```**bar**``` \\*\\*fizz\\*\\* ``` \\*\\*buzz\\*\\*'); + }); + + test('edge-case odd number of backticks with no inline code content', () => { + expect(Util.escapeMarkdown('**foo** `**bar**` **fizz** ` **buzz**', { inlineCode: false, inlineCodeContent: false })) + .toBe('\\*\\*foo\\*\\* `**bar**` \\*\\*fizz\\*\\* ` \\*\\*buzz\\*\\*'); + }); + }); +}); + +/* eslint-enable max-len, no-undef */ diff --git a/typings/index.d.ts b/typings/index.d.ts index b97c62a82..168e84801 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1160,7 +1160,15 @@ declare module 'discord.js' { public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; public static discordSort(collection: Collection): Collection; - public static escapeMarkdown(text: string, onlyCodeBlock?: boolean, onlyInlineCode?: boolean): string; + public static escapeMarkdown(text: string, options?: { codeBlock?: boolean, inlineCode?: boolean, bold?: boolean, italic?: boolean, underline?: boolean, strikethrough?: boolean, spoiler?: boolean, inlineCodeContent?: boolean, codeBlockContent?: boolean }): string; + public static escapeCodeBlock(text: string): string; + public static escapeInlineCode(text: string): string; + public static escapeBold(text: string): string; + public static escapeItalic(text: string): string; + public static escapeUnderline(text: string): string; + public static escapeStrikethrough(text: string): string; + public static escapeSpoiler(text: string): string; + public static cleanCodeBlockContent(text: string): string; public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise; public static flatten(obj: object, ...props: { [key: string]: boolean | string }[]): object; public static idToBinary(num: Snowflake): string; From 547bf8310067503e76ec287a7928b0c44c78ed3e Mon Sep 17 00:00:00 2001 From: MoreThanTom Date: Fri, 12 Jul 2019 16:02:45 +0100 Subject: [PATCH 1116/1359] feat(typings): constants export (#2915) * Added typings for Constants export * Full typing of list Constants * Fix mistake in Package typing * Cleanup for requested changes moved fs import to import cluster WSEvents using WSEventType to build type * Satisfy tslint rules * Update Constants.js * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts * Update index.d.ts --- src/util/Constants.js | 2 +- typings/index.d.ts | 258 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 0587163cd..1286e18c2 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -126,7 +126,7 @@ exports.Endpoints = { return { Emoji: (emojiID, format = 'png') => `${root}/emojis/${emojiID}.${format}`, Asset: name => `${root}/assets/${name}`, - DefaultAvatar: number => `${root}/embed/avatars/${number}.png`, + DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`, Avatar: (userID, hash, format = 'default', size) => { if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 168e84801..7ad1cc06d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2,6 +2,7 @@ declare module 'discord.js' { import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; + import { PathLike } from 'fs'; import * as WebSocket from 'ws'; export const version: string; @@ -378,6 +379,258 @@ declare module 'discord.js' { public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; } + type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'gif'; + + export interface Constants { + Package: { + name: string; + version: string; + description: string; + author: string; + license: string; + main: PathLike; + types: PathLike; + homepage: string; + keywords: string[]; + bugs: { url: string }; + repository: { type: string, url: string }; + browser: { [key: string]: boolean }; + scripts: { [key: string]: string }; + engines: { [key: string]: string }; + dependencies: { [key: string]: string }; + peerDependencies: { [key: string]: string }; + devDependencies: { [key: string]: string }; + [key: string]: any; + }; + browser: boolean; + DefaultOptions: ClientOptions; + UserAgent: string | null; + Endpoints: { + botGateway: string; + invite: (root: string, code: string) => string; + CDN: (root: string) => { + Asset: (name: string) => string; + DefaultAvatar: (id: string | number) => string; + Emoji: (emojiID: string, format: 'png' | 'gif') => string; + Avatar: (userID: string | number, hash: string, format: 'default' | AllowedImageFormat, size: number) => string; + Banner: (guildID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + Icon: (userID: string | number, hash: string, format: 'default' | AllowedImageFormat, size: number) => string; + AppIcon: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + AppAsset: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + GDMIcon: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + Splash: (userID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + TeamIcon: (teamID: string | number, hash: string, format: AllowedImageFormat, size: number) => string; + }; + }; + WSCodes: { + 1000: 'WS_CLOSE_REQUESTED'; + 4004: 'TOKEN_INVALID'; + 4010: 'SHARDING_INVALID'; + 4011: 'SHARDING_REQUIRED'; + }; + Events: { + RATE_LIMIT: 'rateLimit'; + CLIENT_READY: 'ready'; + RESUMED: 'resumed'; + GUILD_CREATE: 'guildCreate'; + GUILD_DELETE: 'guildDelete'; + GUILD_UPDATE: 'guildUpdate'; + GUILD_UNAVAILABLE: 'guildUnavailable'; + GUILD_MEMBER_ADD: 'guildMemberAdd'; + GUILD_MEMBER_REMOVE: 'guildMemberRemove'; + GUILD_MEMBER_UPDATE: 'guildMemberUpdate'; + GUILD_MEMBER_AVAILABLE: 'guildMemberAvailable'; + GUILD_MEMBER_SPEAKING: 'guildMemberSpeaking'; + GUILD_MEMBERS_CHUNK: 'guildMembersChunk'; + GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate'; + GUILD_ROLE_CREATE: 'roleCreate'; + GUILD_ROLE_DELETE: 'roleDelete'; + GUILD_ROLE_UPDATE: 'roleUpdate'; + GUILD_EMOJI_CREATE: 'emojiCreate'; + GUILD_EMOJI_DELETE: 'emojiDelete'; + GUILD_EMOJI_UPDATE: 'emojiUpdate'; + GUILD_BAN_ADD: 'guildBanAdd'; + GUILD_BAN_REMOVE: 'guildBanRemove'; + CHANNEL_CREATE: 'channelCreate'; + CHANNEL_DELETE: 'channelDelete'; + CHANNEL_UPDATE: 'channelUpdate'; + CHANNEL_PINS_UPDATE: 'channelPinsUpdate'; + MESSAGE_CREATE: 'message'; + MESSAGE_DELETE: 'messageDelete'; + MESSAGE_UPDATE: 'messageUpdate'; + MESSAGE_BULK_DELETE: 'messageDeleteBulk'; + MESSAGE_REACTION_ADD: 'messageReactionAdd'; + MESSAGE_REACTION_REMOVE: 'messageReactionRemove'; + MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll'; + USER_UPDATE: 'userUpdate'; + PRESENCE_UPDATE: 'presenceUpdate'; + VOICE_STATE_UPDATE: 'voiceStateUpdate'; + VOICE_BROADCAST_SUBSCRIBE: 'subscribe'; + VOICE_BROADCAST_UNSUBSCRIBE: 'unsubscribe'; + TYPING_START: 'typingStart'; + WEBHOOKS_UPDATE: 'webhookUpdate'; + DISCONNECT: 'disconnect'; + RECONNECTING: 'reconnecting'; + ERROR: 'error'; + WARN: 'warn'; + DEBUG: 'debug'; + SHARD_DISCONNECTED: 'shardDisconnected'; + SHARD_ERROR: 'shardError'; + SHARD_RECONNECTING: 'shardReconnecting'; + SHARD_READY: 'shardReady'; + SHARD_RESUMED: 'shardResumed'; + INVALIDATED: 'invalidated'; + RAW: 'raw'; + }; + ShardEvents: { + CLOSE: 'close'; + DESTROYED: 'destroyed'; + INVALID_SESSION: 'invalidSession'; + READY: 'ready'; + RESUMED: 'resumed'; + }; + PartialTypes: { + [K in PartialType]: K; + }; + WSEvents: { + [K in WSEventType]: K; + }; + Colors: { + DEFAULT: 0x000000; + WHITE: 0xFFFFFF; + AQUA: 0x1ABC9C; + GREEN: 0x2ECC71; + BLUE: 0x3498DB; + YELLOW: 0xFFFF00; + PURPLE: 0x9B59B6; + LUMINOUS_VIVID_PINK: 0xE91E63; + GOLD: 0xF1C40F; + ORANGE: 0xE67E22; + RED: 0xE74C3C; + GREY: 0x95A5A6; + NAVY: 0x34495E; + DARK_AQUA: 0x11806A; + DARK_GREEN: 0x1F8B4C; + DARK_BLUE: 0x206694; + DARK_PURPLE: 0x71368A; + DARK_VIVID_PINK: 0xAD1457; + DARK_GOLD: 0xC27C0E; + DARK_ORANGE: 0xA84300; + DARK_RED: 0x992D22; + DARK_GREY: 0x979C9F; + DARKER_GREY: 0x7F8C8D; + LIGHT_GREY: 0xBCC0C0; + DARK_NAVY: 0x2C3E50; + BLURPLE: 0x7289DA; + GREYPLE: 0x99AAB5; + DARK_BUT_NOT_BLACK: 0x2C2F33; + NOT_QUITE_BLACK: 0x23272A; + }; + Status: { + READY: 0; + CONNECTING: 1; + RECONNECTING: 2; + IDLE: 3; + NEARLY: 4; + DISCONNECTED: 5; + }; + OPCodes: { + DISPATCH: 0; + HEARTBEAT: 1; + IDENTIFY: 2; + STATUS_UPDATE: 3; + VOICE_STATE_UPDATE: 4; + VOICE_GUILD_PING: 5; + RESUME: 6; + RECONNECT: 7; + REQUEST_GUILD_MEMBERS: 8; + INVALID_SESSION: 9; + HELLO: 10; + HEARTBEAT_ACK: 11; + }; + APIErrors: { + UNKNOWN_ACCOUNT: 10001; + UNKNOWN_APPLICATION: 10002; + UNKNOWN_CHANNEL: 10003; + UNKNOWN_GUILD: 10004; + UNKNOWN_INTEGRATION: 10005; + UNKNOWN_INVITE: 10006; + UNKNOWN_MEMBER: 10007; + UNKNOWN_MESSAGE: 10008; + UNKNOWN_OVERWRITE: 10009; + UNKNOWN_PROVIDER: 10010; + UNKNOWN_ROLE: 10011; + UNKNOWN_TOKEN: 10012; + UNKNOWN_USER: 10013; + UNKNOWN_EMOJI: 10014; + UNKNOWN_WEBHOOK: 10015; + BOT_PROHIBITED_ENDPOINT: 20001; + BOT_ONLY_ENDPOINT: 20002; + MAXIMUM_GUILDS: 30001; + MAXIMUM_FRIENDS: 30002; + MAXIMUM_PINS: 30003; + MAXIMUM_ROLES: 30005; + MAXIMUM_REACTIONS: 30010; + UNAUTHORIZED: 40001; + MISSING_ACCESS: 50001; + INVALID_ACCOUNT_TYPE: 50002; + CANNOT_EXECUTE_ON_DM: 50003; + EMBED_DISABLED: 50004; + CANNOT_EDIT_MESSAGE_BY_OTHER: 50005; + CANNOT_SEND_EMPTY_MESSAGE: 50006; + CANNOT_MESSAGE_USER: 50007; + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008; + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009; + OAUTH2_APPLICATION_BOT_ABSENT: 50010; + MAXIMUM_OAUTH2_APPLICATIONS: 50011; + INVALID_OAUTH_STATE: 50012; + MISSING_PERMISSIONS: 50013; + INVALID_AUTHENTICATION_TOKEN: 50014; + NOTE_TOO_LONG: 50015; + INVALID_BULK_DELETE_QUANTITY: 50016; + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019; + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021; + BULK_DELETE_MESSAGE_TOO_OLD: 50034; + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036; + REACTION_BLOCKED: 90001; + }; + VoiceStatus: { + CONNECTED: 0; + CONNECTING: 1; + AUTHENTICATING: 2; + RECONNECTING: 3; + DISCONNECTED: 4; + }; + VoiceOPCodes: { + IDENTIFY: 0; + SELECT_PROTOCOL: 1; + READY: 2; + HEARTBEAT: 3; + SESSION_DESCRIPTION: 4; + SPEAKING: 5; + HELLO: 8; + CLIENT_CONNECT: 12; + CLIENT_DISCONNECT: 13; + }; + ChannelTypes: { + TEXT: 0; + DM: 1; + VOICE: 2; + GROUP: 3; + CATEGORY: 4; + NEWS: 5; + STORE: 6; + }; + ClientApplicationAssetTypes: { + SMALL: 1; + BIG: 2; + }; + MessageTypes: MessageType[]; + ActivityTypes: ActivityType[]; + DefaultMessageNotifications: DefaultMessageNotifications[]; + MembershipStates: 'INVITED' | 'ACCEPTED'; + } + export class DataResolver { public static resolveBase64(data: Base64Resolvable): string; public static resolveFile(resource: BufferResolvable | Stream): Promise; @@ -2290,6 +2543,11 @@ declare module 'discord.js' { compress?: boolean; } + type PartialType = 'USER' + | 'CHANNEL' + | 'GUILD_MEMBER' + | 'MESSAGE'; + type WSEventType = 'READY' | 'RESUMED' | 'GUILD_CREATE' From 651ff81bd522c03b5c9724a3a8c14eeb88dcd6b9 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 13 Jul 2019 18:02:07 +0200 Subject: [PATCH 1117/1359] fix: update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +++- .github/ISSUE_TEMPLATE/feature_request.md | 4 +++- .github/ISSUE_TEMPLATE/question---general-support-request.md | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2cfa22985..d5061af2b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,9 @@ --- name: Bug report about: Report incorrect or unexpected behaviour of discord.js -labels: s: unverified, type: bug +title: '' +labels: 's: unverified, type: bug' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 45ef822bf..4d021ad36 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,9 @@ --- name: Feature request about: Request a feature for the core discord.js library -labels: type: enhancement +title: '' +labels: 'type: enhancement' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md index aa3153ea2..d2cef51f1 100644 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ b/.github/ISSUE_TEMPLATE/question---general-support-request.md @@ -1,7 +1,9 @@ --- name: Question / General support request about: Ask for help in Discord instead - https://discord.gg/bRCvFy9 +title: '' labels: question (please use Discord instead) +assignees: '' --- From d8516efa36f62ef8a6e33b0b35ba5f482e22d103 Mon Sep 17 00:00:00 2001 From: Kitten King Date: Thu, 25 Jul 2019 20:48:23 +0530 Subject: [PATCH 1118/1359] docs: fix typos (#3404) --- src/client/websocket/WebSocketManager.js | 2 +- src/client/websocket/WebSocketShard.js | 2 +- src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- src/sharding/Shard.js | 2 +- src/sharding/ShardingManager.js | 2 +- travis/deploy.sh | 4 ++-- typings/index.d.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 6dec7c40e..c2530ef31 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -297,7 +297,7 @@ class WebSocketManager extends EventEmitter { } catch (error) { this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); if (error.httpStatus !== 401) { - this.debug(`Possible network error occured. Retrying in 5s...`); + this.debug(`Possible network error occurred. Retrying in 5s...`); await Util.delayFor(5000); this.reconnecting = false; return this.reconnect(); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 13ffa9f6d..9663052c6 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -465,7 +465,7 @@ class WebSocketShard extends EventEmitter { */ sendHeartbeat() { if (!this.lastHeartbeatAcked) { - this.debug("Didn't receive a heartbeat ack last time, assuming zombie conenction. Destroying and reconnecting."); + this.debug("Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting."); this.destroy(4009); return; } diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index 11154674a..da73693cb 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -14,7 +14,7 @@ module.exports = (client, { d: data }) => { * Emitted whenever the pins of a channel are updated. Due to the nature of the WebSocket event, * not much information can be provided easily here - you need to manually check the pins yourself. * @event Client#channelPinsUpdate - * @param {DMChannel|TextChannel} channel The channel that the pins update occured in + * @param {DMChannel|TextChannel} channel The channel that the pins update occurred in * @param {Date} time The time of the pins update */ client.emit(Events.CHANNEL_PINS_UPDATE, channel, time); diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 4fab578d8..6533b4f83 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -319,7 +319,7 @@ class Shard extends EventEmitter { } /** - * Emitted upon recieving a message from the child process/worker. + * Emitted upon receiving a message from the child process/worker. * @event Shard#message * @param {*} message Message that was received */ diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 15b74ffdf..f0298054c 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -73,7 +73,7 @@ class ShardingManager extends EventEmitter { if (this.shardList.length < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardList', 'at least 1 ID.'); if (this.shardList.some(shardID => typeof shardID !== 'number' || isNaN(shardID) || !Number.isInteger(shardID) || shardID < 0)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of postive integers.'); + throw new TypeError('CLIENT_INVALID_OPTION', 'shardList', 'an array of positive integers.'); } } diff --git a/travis/deploy.sh b/travis/deploy.sh index 39b579099..7043abf98 100644 --- a/travis/deploy.sh +++ b/travis/deploy.sh @@ -11,7 +11,7 @@ fi DONT_COMMIT=false if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then - echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - not commiting" + echo -e "\e[36m\e[1mBuild triggered for PR #${TRAVIS_PULL_REQUEST} to branch \"${TRAVIS_BRANCH}\" - not committing" SOURCE_TYPE="pr" DONT_COMMIT=true elif [ -n "$TRAVIS_TAG" ]; then @@ -29,7 +29,7 @@ npm run docs NODE_ENV=production npm run build:browser if [ $DONT_COMMIT == true ]; then - echo -e "\e[36m\e[1mNot commiting - exiting early" + echo -e "\e[36m\e[1mNot committing - exiting early" exit 0 fi diff --git a/typings/index.d.ts b/typings/index.d.ts index 7ad1cc06d..76c8f4e89 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1736,7 +1736,7 @@ declare module 'discord.js' { public create(name: string, options?: GuildCreateChannelOptions): Promise; } - // Hacky workaround because changing the signature of an overriden method errors + // Hacky workaround because changing the signature of an overridden method errors class OverridableDataStore, R = any> extends DataStore { public add(data: any, cache: any): any; public set(key: any): any; From e562564123912f09725a6e03ecf4ba44d85de35a Mon Sep 17 00:00:00 2001 From: Alex <41408947+S0ftwareUpd8@users.noreply.github.com> Date: Sun, 28 Jul 2019 06:24:27 -0700 Subject: [PATCH 1119/1359] docs(Guild): add missing features (#3406) * Update Guild.js * Update Guild.js * style(Guild): remove trailing space * typings(Guild): add new features --- src/structures/Guild.js | 2 ++ typings/index.d.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 90728501f..21be50d14 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -159,6 +159,8 @@ class Guild extends Base { * * VERIFIED * * VIP_REGIONS * * VANITY_URL + * * DISCOVERABLE + * * FEATURABLE * @typedef {string} Features */ diff --git a/typings/index.d.ts b/typings/index.d.ts index 76c8f4e89..ad9b150e2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2223,7 +2223,9 @@ declare module 'discord.js' { | 'MORE_EMOJI' | 'VERIFIED' | 'VIP_REGIONS' - | 'VANITY_URL'; + | 'VANITY_URL' + | 'DISCOVERABLE' + | 'FEATURABLE'; interface GuildMemberEditData { nick?: string; From 53722b47c1d646a23f4eb422c0256eaef699383b Mon Sep 17 00:00:00 2001 From: Lewis Date: Mon, 29 Jul 2019 23:00:56 +0100 Subject: [PATCH 1120/1359] chore(license): update copyright notice (#3408) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 5f32c7958..9a4257e6c 100644 --- a/LICENSE +++ b/LICENSE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright 2015 - 2018 Amish Shah + Copyright 2015 - 2019 Amish Shah Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From e645dd6358d1ad60f81a919ddfebf7db2092b1bc Mon Sep 17 00:00:00 2001 From: TNThacker2015 <37024464+TNThacker2015@users.noreply.github.com> Date: Mon, 29 Jul 2019 15:25:35 -0700 Subject: [PATCH 1121/1359] feat: Util.splitMessage always return an array (#3035) * Making Util.splitMessage always return an array Util.splitMessage sometimes returns an array, but other times it returns a string. This should make it so that it always returns an array. * jsdoc Co-Authored-By: TNThacker2015 <37024464+TNThacker2015@users.noreply.github.com> * docs(Util): remove superfluous space in docstring --- src/util/Util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 2f12d5870..f9aaf21c2 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -53,11 +53,11 @@ class Util { * Splits a string into multiple chunks at a designated character that do not exceed a specific length. * @param {StringResolvable} text Content to split * @param {SplitOptions} [options] Options controlling the behavior of the split - * @returns {string|string[]} + * @returns {string[]} */ static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { text = this.resolveString(text); - if (text.length <= maxLength) return text; + if (text.length <= maxLength) return [text]; const splitText = text.split(char); if (splitText.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); const messages = []; From 5af8cb8e6e7591e81f758a8b0f6748db7c2f12f1 Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 30 Jul 2019 00:25:45 +0200 Subject: [PATCH 1122/1359] feat: overload for split always returning an array (#3411) * feat: overload for split always returning an array * feat: update Util.splitMessage --- typings/index.d.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index ad9b150e2..43a607ab6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1440,7 +1440,7 @@ declare module 'discord.js' { route: object, reason?: string ): Promise<{ id: Snowflake; position: number }[]>; - public static splitMessage(text: string, options?: SplitOptions): string | string[]; + public static splitMessage(text: string, options?: SplitOptions): string[]; public static str2ab(str: string): ArrayBuffer; } @@ -1828,8 +1828,10 @@ declare module 'discord.js' { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date; - send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; } interface TextBasedChannelFields extends PartialTextBasedChannelFields { @@ -1850,8 +1852,10 @@ declare module 'discord.js' { token: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions | MessageAdditions): Promise; - send(options?: WebhookMessageOptions | MessageAdditions | APIMessage): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + send(options?: WebhookMessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + send(options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; sendSlackMessage(body: object): Promise; } From d14db521585f2b6b1a6c50d3a9acecc73bea25a3 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 4 Aug 2019 15:57:39 +0200 Subject: [PATCH 1123/1359] fix(typings): send overloads --- typings/index.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 43a607ab6..09cc75ca5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1828,10 +1828,12 @@ declare module 'discord.js' { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date; + send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; send(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; - send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; } interface TextBasedChannelFields extends PartialTextBasedChannelFields { From 9e76f233143f2f0fe0ff9a9e645f32dc8db44b26 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sat, 10 Aug 2019 13:22:11 +0300 Subject: [PATCH 1124/1359] fix(GuildMemberStore): reject BAN_RESOLVE_ID error instead of throwing it (#3425) --- src/stores/GuildMemberStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 9f9c42117..24946841a 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -177,7 +177,7 @@ class GuildMemberStore extends DataStore { */ unban(user, reason) { const id = this.client.users.resolveID(user); - if (!id) throw new Error('BAN_RESOLVE_ID'); + if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID')); return this.client.api.guilds(this.guild.id).bans[id].delete({ reason }) .then(() => this.client.users.resolve(user)); } From f79f0243438a89c4d8a910461800b1adff2fe6e5 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 12 Aug 2019 21:40:26 +0100 Subject: [PATCH 1125/1359] fix: bots being unable to connect --- src/rest/RESTManager.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index 306fa2468..1e0b1ecff 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -27,9 +27,7 @@ class RESTManager { getAuth() { const token = this.client.token || this.client.accessToken; - const prefixed = !!this.client.application || this.client.user; - if (token && prefixed) return `${this.tokenPrefix} ${token}`; - else if (token) return token; + if (token) return `${this.tokenPrefix} ${token}`; throw new Error('TOKEN_MISSING'); } From 2c4d14a71be11c36dfa661fd4dc013a381ef3988 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 14 Aug 2019 19:22:41 +0100 Subject: [PATCH 1126/1359] voice: remove redundant debug info --- src/client/voice/VoiceConnection.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b7de316b3..67e79a3b1 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -188,13 +188,12 @@ class VoiceConnection extends EventEmitter { self_deaf: this.voice ? this.voice.selfDeaf : false, }, options); - const queueLength = this.channel.guild.shard.ratelimit.queue.length; - this.emit('debug', `Sending voice state update (queue length is ${queueLength}): ${JSON.stringify(options)}`); + this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`); return this.channel.guild.shard.send({ op: OPCodes.VOICE_STATE_UPDATE, d: options, - }); + }, true); } /** From c6e8fccbf07a2ba2ac80d7e13f967eaa07c881f8 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Sat, 17 Aug 2019 13:42:22 +0100 Subject: [PATCH 1127/1359] voice: fix #3418 (kicking bot from voice channel doesn't allow it to rejoin) --- src/client/actions/VoiceStateUpdate.js | 2 +- src/client/voice/ClientVoiceManager.js | 4 ++-- src/client/voice/VoiceConnection.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 045ca9319..14e1aa3f8 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -25,7 +25,7 @@ class VoiceStateUpdate extends Action { } // Emit event - if (member && member.user.id === client.user.id && data.channel_id) { + if (member && member.user.id === client.user.id) { client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`); client.voice.onVoiceStateUpdate(data); } diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 2ca2b465a..784136934 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -1,7 +1,6 @@ 'use strict'; const Collection = require('../../util/Collection'); -const { VoiceStatus } = require('../../util/Constants'); const VoiceConnection = require('./VoiceConnection'); const VoiceBroadcast = require('./VoiceBroadcast'); const { Error } = require('../../errors'); @@ -52,8 +51,9 @@ class ClientVoiceManager { const connection = this.connections.get(guild_id); this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`); if (!connection) return; - if (!channel_id && connection.status !== VoiceStatus.DISCONNECTED) { + if (!channel_id) { connection._disconnect(); + this.connections.delete(guild_id); return; } connection.channel = this.client.channels.get(channel_id); diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 67e79a3b1..b56177c64 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -388,6 +388,7 @@ class VoiceConnection extends EventEmitter { ws.removeAllListeners('ready'); ws.removeAllListeners('sessionDescription'); ws.removeAllListeners('speaking'); + ws.shutdown(); } if (udp) udp.removeAllListeners('error'); From f55e4302c9bc47427d0d9fc618ff4bcad00da87c Mon Sep 17 00:00:00 2001 From: Robin Millette Date: Sat, 17 Aug 2019 11:50:49 -0400 Subject: [PATCH 1128/1359] github: fix duplicate key in FUNDING.yml (#3380) Comment out (keep for reference) unused platforms. Also fixes a duplicate `github` key. --- .github/FUNDING.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a0273f117..6aa35f01b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,11 +1,11 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: # Replace with a single custom sponsorship URL +# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +# open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# custom: # Replace with a single custom sponsorship URL github: amishshah patreon: discordjs From 7fae6e5bca8651fc28face08386ba876ab663cf1 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyk0@users.noreply.github.com> Date: Sat, 17 Aug 2019 09:51:52 -0600 Subject: [PATCH 1129/1359] typings: switch overloads of RoleStore#fetch (#3397) because compu told me to --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 09cc75ca5..51b0d993c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1797,8 +1797,8 @@ declare module 'discord.js' { public readonly highest: Role; public create(options?: { data?: RoleData, reason?: string }): Promise; - public fetch(id?: Snowflake, cache?: boolean): Promise; public fetch(id: Snowflake, cache?: boolean): Promise; + public fetch(id?: Snowflake, cache?: boolean): Promise; } export class UserStore extends DataStore { From 6d3c55b68cbdb258421e017577f52da0d8deaf85 Mon Sep 17 00:00:00 2001 From: bdistin Date: Sat, 17 Aug 2019 10:57:45 -0500 Subject: [PATCH 1130/1359] feat(Collector): allow collectors to be consumed by for-await-of loops (#3269) --- src/structures/interfaces/Collector.js | 31 ++++++++++++++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 32 insertions(+) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index cadb44c65..3cd5d387e 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -196,6 +196,37 @@ class Collector extends EventEmitter { if (reason) this.stop(reason); } + /** + * Allows collectors to be consumed with for-await-of loops + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of} + */ + async *[Symbol.asyncIterator]() { + const queue = []; + const onCollect = item => queue.push(item); + this.on('collect', onCollect); + + try { + while (queue.length || !this.ended) { + if (queue.length) { + yield queue.shift(); + } else { + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => { + const tick = () => { + this.off('collect', tick); + this.off('end', tick); + return resolve(); + }; + this.on('collect', tick); + this.on('end', tick); + }); + } + } + } finally { + this.off('collect', onCollect); + } + } + toJSON() { return Util.flatten(this); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 51b0d993c..0d2422b63 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -363,6 +363,7 @@ declare module 'discord.js' { public handleCollect(...args: any[]): void; public handleDispose(...args: any[]): void; public stop(reason?: string): void; + public [Symbol.asyncIterator](): AsyncIterableIterator; public toJSON(): object; protected listener: Function; From 2df4f227a446a48330e941762374a262e138d2fd Mon Sep 17 00:00:00 2001 From: didinele Date: Sat, 17 Aug 2019 19:02:17 +0300 Subject: [PATCH 1131/1359] refactor: move Guild#defaultRole to RoleStore#everyone (#3347) * remove guild#defaultRole * add RoleStore#defaultRole * typings * fix trailing space * another one * Rename it to everyone --- src/stores/RoleStore.js | 9 +++++++++ src/structures/Guild.js | 9 --------- typings/index.d.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index db3e2c0be..649048be2 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -110,6 +110,15 @@ class RoleStore extends DataStore { }); } + /** + * The `@everyone` role of the guild + * @type {?Role} + * @readonly + */ + get everyone() { + return this.get(this.guild.id) || null; + } + /** * The role with the highest position in the store * @type {Role} diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 21be50d14..04aece2ff 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -489,15 +489,6 @@ class Guild extends Base { return this.client.channels.get(this.embedChannelID) || null; } - /** - * The `@everyone` role of the guild - * @type {?Role} - * @readonly - */ - get defaultRole() { - return this.roles.get(this.id) || null; - } - /** * The client user as a GuildMember of this guild * @type {?GuildMember} diff --git a/typings/index.d.ts b/typings/index.d.ts index 0d2422b63..838eb2b3e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -686,7 +686,6 @@ declare module 'discord.js' { public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; - public readonly defaultRole: Role | null; public deleted: boolean; public description: string | null; public embedChannel: GuildChannel | null; @@ -1795,6 +1794,7 @@ declare module 'discord.js' { export class RoleStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); + public readonly everyone: Role | null; public readonly highest: Role; public create(options?: { data?: RoleData, reason?: string }): Promise; From b662678f210ba893a2bb2c27ba781b2bf3ccfc16 Mon Sep 17 00:00:00 2001 From: BannerBomb Date: Sat, 17 Aug 2019 12:07:58 -0400 Subject: [PATCH 1132/1359] feat/fix(Util): fix animated part of parseEmoji regex and make id optional (#3407) * Small changes to parseEmoji regex I just made a small change to the parseEmoji regex, this change will make an invalid emoji, like `` return as null, before this change it would return as an animated emoji because the name started with an `a` which would result in false positives, then the `?` I added to the end of `(\d{17,19})?` is used if someone provided an emoji as `:name:` or `a:name:` it will return the correct values but have an invalid id. * Update Util.js 2nd Update: I changed the regex to output the results if you provide `` and <:aemoji:123456789012345678>` which will output `{ animated: false, name: "aemoji", id: "123456789012345678" }` or `<:emojiname:>` which outputs `{ animated: false, name: "emojiname", id: null }` or `` which would output `{ animated: true, name: "emoji", id: null }`. Before this PR the method would return that the emoji was animated if you provided something like `` because the name started with an `a`. --- src/util/Util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index f9aaf21c2..8e6743d9f 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -244,9 +244,9 @@ class Util { static parseEmoji(text) { if (text.includes('%')) text = decodeURIComponent(text); if (!text.includes(':')) return { animated: false, name: text, id: null }; - const m = text.match(/?/); + const m = text.match(/?/); if (!m) return null; - return { animated: Boolean(m[1]), name: m[2], id: m[3] }; + return { animated: Boolean(m[1]), name: m[2], id: m[3] || null }; } /** From ab27dd0218c82492d3f0f5665a8a169af4d4641a Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Sat, 17 Aug 2019 12:24:16 -0400 Subject: [PATCH 1133/1359] refactor(TeamMember): remove client from constructor (#3409) * Remove client from TeamMember constructor part 1 * Remove client from TeamMember constructor part 2 * update typings --- src/structures/Team.js | 2 +- src/structures/TeamMember.js | 4 ++-- typings/index.d.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structures/Team.js b/src/structures/Team.js index cec984b6b..25b13facd 100644 --- a/src/structures/Team.js +++ b/src/structures/Team.js @@ -47,7 +47,7 @@ class Team extends Base { this.members = new Collection(); for (const memberData of data.members) { - const member = new TeamMember(this.client, this, memberData); + const member = new TeamMember(this, memberData); this.members.set(member.id, member); } } diff --git a/src/structures/TeamMember.js b/src/structures/TeamMember.js index 03bcf6ec2..ba7ecd266 100644 --- a/src/structures/TeamMember.js +++ b/src/structures/TeamMember.js @@ -8,8 +8,8 @@ const { MembershipStates } = require('../util/Constants'); * @extends {Base} */ class TeamMember extends Base { - constructor(client, team, data) { - super(client); + constructor(team, data) { + super(team.client); /** * The Team this member is part of diff --git a/typings/index.d.ts b/typings/index.d.ts index 838eb2b3e..3cb0c622a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -283,7 +283,7 @@ declare module 'discord.js' { } export class TeamMember extends Base { - constructor(client: Client, team: Team, data: object); + constructor(team: Team, data: object); public team: Team; public readonly id: Snowflake; public permissions: string[]; From 8ae7a30d0b4df8b6833bc754aaadbea8d75130ef Mon Sep 17 00:00:00 2001 From: Khoo Hao Yit <40757009+KhooHaoYit@users.noreply.github.com> Date: Sun, 18 Aug 2019 01:33:03 +0800 Subject: [PATCH 1134/1359] fix(Message): delete method caused messageDelete event to fire twice (#3252) * ref: add getPayload and use for other get* methods * return existing data.* * use Action.getUser() * Fix messageDelete double emission --- src/structures/Message.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 8e5f423ab..99c006b7f 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -454,11 +454,7 @@ class Message extends Base { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.channel.messages.remove(this.id, reason).then(() => - this.client.actions.MessageDelete.handle({ - id: this.id, - channel_id: this.channel.id, - }).message); + return this.channel.messages.remove(this.id, reason).then(() => this); } else { return new Promise(resolve => { this.client.setTimeout(() => { From d62db232e792ef698f16d903e075d8977136624f Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sat, 17 Aug 2019 19:31:04 +0100 Subject: [PATCH 1135/1359] feat(Invite): add targetUser(Type) (#3262) * add Invite#targetUser(Type) * incase discord decides to add 0 --- src/structures/Invite.js | 18 ++++++++++++++++++ typings/index.d.ts | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index b73076a60..d5e9f895d 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -70,6 +70,24 @@ class Invite extends Base { */ this.inviter = data.inviter ? this.client.users.add(data.inviter) : null; + /** + * The target user for this invite + * @type {?User} + */ + this.targetUser = data.target_user ? this.client.users.add(data.target_user) : null; + + /** + * The type of the target user: + * * 1: STREAM + * @typedef {number} TargetUser + */ + + /** + * The target user type + * @type {?TargetUser} + */ + this.targetUserType = typeof data.target_user_type === 'number' ? data.target_user_type : null; + /** * The channel the invite is for * @type {Channel} diff --git a/typings/index.d.ts b/typings/index.d.ts index 3cb0c622a..877dfed32 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -928,6 +928,8 @@ declare module 'discord.js' { public maxUses: number | null; public memberCount: number; public presenceCount: number; + public targetUser: User | null; + public targetUserType: TargetUser | null; public temporary: boolean | null; public readonly url: string; public uses: number | null; @@ -2524,6 +2526,8 @@ declare module 'discord.js' { type StringResolvable = string | string[] | any; + type TargetUser = number; + type UserResolvable = User | Snowflake | Message | GuildMember; type VoiceStatus = number; From 12b48b7cbb74b649d523fea40237b3cace66fff8 Mon Sep 17 00:00:00 2001 From: Koyamie Date: Sat, 17 Aug 2019 20:57:14 +0200 Subject: [PATCH 1136/1359] fix(GuildMemberRoleStore): correctly reference the everyone role (#3434) --- src/stores/GuildMemberRoleStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 824bca686..4c63bed83 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -23,7 +23,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get _filtered() { - const everyone = this.guild.defaultRole; + const everyone = this.guild.roles.everyone; return this.guild.roles.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); } From 1851f747707d2863ab507a679b8345f6bcc05d0e Mon Sep 17 00:00:00 2001 From: Khoo Hao Yit <40757009+KhooHaoYit@users.noreply.github.com> Date: Sun, 18 Aug 2019 03:09:29 +0800 Subject: [PATCH 1137/1359] fix(ReactionUserStore): remove method firing messageReactionRemove event twice (#3277) --- src/stores/ReactionUserStore.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/stores/ReactionUserStore.js b/src/stores/ReactionUserStore.js index 976b79917..dc250a9fb 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/stores/ReactionUserStore.js @@ -48,14 +48,7 @@ class ReactionUserStore extends DataStore { return message.client.api.channels[message.channel.id].messages[message.id] .reactions[this.reaction.emoji.identifier][userID === message.client.user.id ? '@me' : userID] .delete() - .then(() => - message.client.actions.MessageReactionRemove.handle({ - user_id: userID, - message_id: message.id, - emoji: this.reaction.emoji, - channel_id: message.channel.id, - }).reaction - ); + .then(() => this.reaction); } } From c786867bd65d8f6661e3de9c9528768d1ec03d46 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 18 Aug 2019 11:45:28 +0200 Subject: [PATCH 1138/1359] fix(Webhook): return raw data if the channel is unavailable Fixes #3424 --- src/structures/Webhook.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 6c1a3db50..2dc1a0282 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -146,8 +146,9 @@ class Webhook { query: { wait: true }, auth: false, }).then(d => { - if (!this.client.channels) return d; - return this.client.channels.get(d.channel_id).messages.add(d, false); + const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; + if (!channel) return d; + return channel.messages.add(d, false); }); } @@ -173,9 +174,10 @@ class Webhook { query: { wait: true }, auth: false, data: body, - }).then(data => { - if (!this.client.channels) return data; - return this.client.channels.get(data.channel_id).messages.add(data, false); + }).then(d => { + const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; + if (!channel) return d; + return channel.messages.add(d, false); }); } From 3fcc862c5fa336bb26c570655245d57e2e532f20 Mon Sep 17 00:00:00 2001 From: neoney <30625554+n3oney@users.noreply.github.com> Date: Sun, 18 Aug 2019 11:54:30 +0200 Subject: [PATCH 1139/1359] docs: fix voice broadcast example code (#3436) client.createVoiceBroadcast() -> client.voice.createBroadcast() --- docs/topics/voice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index e48b8b325..6a39f5a3f 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -114,7 +114,7 @@ Make sure to consult the documentation for a full list of what you can play - th A voice broadcast is very useful for "radio" bots, that play the same audio across multiple channels. It means audio is only transcoded once, and is much better on performance. ```js -const broadcast = client.createVoiceBroadcast(); +const broadcast = client.voice.createBroadcast(); broadcast.on('subscribe', dispatcher => { console.log('New broadcast subscriber!'); From e4309b23d5f46133c79e5ff3692a13bc8c61cbdb Mon Sep 17 00:00:00 2001 From: Saya <36309350+Deivu@users.noreply.github.com> Date: Tue, 20 Aug 2019 00:55:07 +0800 Subject: [PATCH 1140/1359] feat: abort Requests that takes a lot of time to resolve (#3327) * Add Request Timeout * Add abort controller in packages * Fix Lint Error. * Fix Lint Errors * Make Timeout Customizable & use finally * Fixed a minor issue * Fix eslint * Update request timeout to use d.js client timeout methods. --- package.json | 1 + src/client/Client.js | 3 +++ src/rest/APIRequest.js | 6 +++++- src/util/Constants.js | 2 ++ typings/index.d.ts | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c454decf..c234bfb7f 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { + "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", "pako": "^1.0.8", diff --git a/src/client/Client.js b/src/client/Client.js index 85a93bb8a..3be8e3115 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -392,6 +392,9 @@ class Client extends BaseClient { if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restWsBridgeTimeout', 'a number'); } + if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) { + throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number'); + } if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) { throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number'); } diff --git a/src/rest/APIRequest.js b/src/rest/APIRequest.js index 672cccd4e..520febca0 100644 --- a/src/rest/APIRequest.js +++ b/src/rest/APIRequest.js @@ -4,6 +4,7 @@ const FormData = require('form-data'); const https = require('https'); const { browser, UserAgent } = require('../util/Constants'); const fetch = require('node-fetch'); +const AbortController = require('abort-controller'); if (https.Agent) var agent = new https.Agent({ keepAlive: true }); @@ -46,12 +47,15 @@ class APIRequest { headers['Content-Type'] = 'application/json'; } + const controller = new AbortController(); + const timeout = this.client.setTimeout(() => controller.abort(), this.client.options.restRequestTimeout); return fetch(url, { method: this.method, headers, agent, body, - }); + signal: controller.signal, + }).finally(() => this.client.clearTimeout(timeout)); } } diff --git a/src/util/Constants.js b/src/util/Constants.js index 1286e18c2..50033919d 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -28,6 +28,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST * requests (higher values will reduce rate-limiting errors on bad connections) + * @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request * @property {number} [restSweepInterval=60] How frequently to delete inactive request buckets, in seconds * (or 0 for never) * @property {number} [retryLimit=1] How many times to retry on 5XX errors (Infinity for indefinite amount of retries) @@ -50,6 +51,7 @@ exports.DefaultOptions = { partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], + restRequestTimeout: 15000, retryLimit: 1, restTimeOffset: 500, restSweepInterval: 60, diff --git a/typings/index.d.ts b/typings/index.d.ts index 877dfed32..0bc65d4e5 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2018,6 +2018,7 @@ declare module 'discord.js' { partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; + restRequestTimeout?: number; restSweepInterval?: number; retryLimit?: number; presence?: PresenceData; From fbd811517a298b857f9fd963d8fb13c4c1207f90 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 19 Aug 2019 23:02:33 +0300 Subject: [PATCH 1141/1359] src: Update Webhook#sendSlackMessage to be accurate with what the API returns (#3429) * src: Update sendSlackMessage * typings --- src/structures/Webhook.js | 8 ++------ typings/index.d.ts | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 2dc1a0282..2ed6137b4 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -155,7 +155,7 @@ class Webhook { /** * Sends a raw slack message with this webhook. * @param {Object} body The raw body to send - * @returns {Promise} + * @returns {Promise} * @example * // Send a slack message * webhook.sendSlackMessage({ @@ -174,11 +174,7 @@ class Webhook { query: { wait: true }, auth: false, data: body, - }).then(d => { - const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; - if (!channel) return d; - return channel.messages.add(d, false); - }); + }).then(data => data.toString() === 'ok'); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 0bc65d4e5..72ee1e893 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1861,7 +1861,7 @@ declare module 'discord.js' { send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; send(options?: WebhookMessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; send(options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; - sendSlackMessage(body: object): Promise; + sendSlackMessage(body: object): Promise; } //#endregion From 5e4f9d436d33129ec7d2dfdf0c5d014d623c1e93 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 21 Aug 2019 14:52:35 +0300 Subject: [PATCH 1142/1359] src: alphabetize guild features and make sure they're up to date (#3441) --- src/structures/Guild.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 04aece2ff..f24f08e68 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -149,18 +149,17 @@ class Guild extends Base { /** * An array of enabled guild features, here are the possible values: * * ANIMATED_ICON - * * COMMERCE - * * LURKABLE - * * PARTNERED - * * NEWS * * BANNER - * * INVITE_SPLASH - * * MORE_EMOJI - * * VERIFIED - * * VIP_REGIONS - * * VANITY_URL + * * COMMERCE * * DISCOVERABLE * * FEATURABLE + * * INVITE_SPLASH + * * LURKABLE + * * NEWS + * * PARTNERED + * * VANITY_URL + * * VERIFIED + * * VIP_REGIONS * @typedef {string} Features */ From cc488a8bd3dc39354967d132e34f63ff51e92b79 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyk0@users.noreply.github.com> Date: Wed, 21 Aug 2019 09:52:08 -0600 Subject: [PATCH 1143/1359] fix: GuildMemberStore#_fetchMany (#3420) * added DARK_MODE_INVISIBLE added another constant color that makes embeds appear invisible on DARK mode. * travis likes trailing commas * fix: ref issue: #3414 * fix: removed a random color --- src/stores/GuildMemberStore.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index 24946841a..e02aa3dcc 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -192,8 +192,8 @@ class GuildMemberStore extends DataStore { _fetchMany({ query = '', limit = 0 } = {}) { return new Promise((resolve, reject) => { - if (this.guild.memberCount === this.size) { - resolve(query || limit ? new Collection() : this); + if (this.guild.memberCount === this.size && !query && !limit) { + resolve(this); return; } this.guild.shard.send({ From c715ed9f8b6a7487d335901cf4b06d276069962b Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Thu, 22 Aug 2019 12:15:20 +0100 Subject: [PATCH 1144/1359] voice: remove passes (discord will begin dropping duplicated audio packets from tomorrow, you should not set passes > 1) --- docs/topics/voice.md | 5 +--- .../voice/dispatcher/StreamDispatcher.js | 23 ++++++++----------- src/client/voice/util/PlayInterface.js | 1 - typings/index.d.ts | 1 - 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index 6a39f5a3f..22f5c93de 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -70,13 +70,10 @@ We can also pass in options when we first play the stream: ```js const dispatcher = connection.play('/home/discord/audio.mp3', { - volume: 0.5, - passes: 3 + volume: 0.5 }); ``` -These are just a subset of the options available (consult documentation for a full list). Most users may be interested in the `passes` option, however. As audio is sent over UDP, there is a chance packets may not arrive. Increasing the number of passes, e.g. to `3` gives you a better chance that your packets reach your recipients, at the cost of triple the bandwidth. We recommend not going over 5 passes. - ### What can I play? Discord.js allows you to play a lot of things: diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 0a3f5220a..2923eb61e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -33,9 +33,9 @@ const nonce = Buffer.alloc(24); class StreamDispatcher extends Writable { constructor( player, - { seek = 0, volume = 1, passes = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, + { seek = 0, volume = 1, fec, plp, bitrate = 96, highWaterMark = 12 } = {}, streams) { - const streamOptions = { seek, volume, passes, fec, plp, bitrate, highWaterMark }; + const streamOptions = { seek, volume, fec, plp, bitrate, highWaterMark }; super(streamOptions); /** * The Audio Player that controls this dispatcher @@ -289,24 +289,21 @@ class StreamDispatcher extends Writable { } _sendPacket(packet) { - let repeats = this.streamOptions.passes; /** * Emitted whenever the dispatcher has debug information. * @event StreamDispatcher#debug * @param {string} info The debug info */ this._setSpeaking(1); - while (repeats--) { - if (!this.player.voiceConnection.sockets.udp) { - this.emit('debug', 'Failed to send a packet - no UDP socket'); - return; - } - this.player.voiceConnection.sockets.udp.send(packet) - .catch(e => { - this._setSpeaking(0); - this.emit('debug', `Failed to send a packet - ${e}`); - }); + if (!this.player.voiceConnection.sockets.udp) { + this.emit('debug', 'Failed to send a packet - no UDP socket'); + return; } + this.player.voiceConnection.sockets.udp.send(packet) + .catch(e => { + this._setSpeaking(0); + this.emit('debug', `Failed to send a packet - ${e}`); + }); } _setSpeaking(value) { diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index 873cd81e9..6f1ba018f 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -11,7 +11,6 @@ const { Error } = require('../../../errors'); * @property {number} [seek=0] The time to seek to, will be ignored when playing `ogg/opus` or `webm/opus` streams * @property {number|boolean} [volume=1] The volume to play at. Set this to false to disable volume transforms for * this stream to improve performance. - * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss * @property {number} [plp] Expected packet loss percentage * @property {boolean} [fec] Enabled forward error correction * @property {number|string} [bitrate=96] The bitrate (quality) of the audio in kbps. diff --git a/typings/index.d.ts b/typings/index.d.ts index 72ee1e893..c6a69c911 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2514,7 +2514,6 @@ declare module 'discord.js' { type?: StreamType; seek?: number; volume?: number | boolean; - passes?: number; plp?: number; fec?: boolean; bitrate?: number | 'auto'; From e8451561ed742a0b49775591c57d37a6e927287f Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 24 Aug 2019 19:32:18 +0200 Subject: [PATCH 1145/1359] ci: github actions (#3442) * feat: eslint action * refactor: actions v2 * fix: set +x for entrypoint * ci: integrate docs * fix: give it a nice name * ci: publish docs * ci: fix yaml key error * ci: fix executable * ci: use normal sh shebang * ci: move into the workspace * ci: fix eslint path * ci: manually run the build * ci: use different container * ci: install git * ci: assume yes flag * ci: use correct branch * ci: fix branch and source * ci: fix condition * ci: remove useless steps * ci: rename action * ci: make executable again * ci: cleanup * ci: add stable branch * ci: remove semi * ci: do some logging for failing action * ci: remove actions api * ci: re-add semi * ci: use actions repo * ci: use v1 tags * ci: remove semi * chore: change job name passed to eslint * chore: dummy commit, remove different semi * ci: change job name * chore: dummy commit * ci: add gh-actions as possible branches * ci: lint all branches * ci: dummy * ci: separate pr and push * chore: run actions on gh branch * ci: try excluding branches --- .github/actions/docs/Dockerfile | 11 ++++++ .github/actions/docs/src/entrypoint.sh | 54 ++++++++++++++++++++++++++ .github/workflows/docs.yml | 25 ++++++++++++ .github/workflows/lint.yml | 28 +++++++++++++ travis/test.sh | 2 +- 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 .github/actions/docs/Dockerfile create mode 100755 .github/actions/docs/src/entrypoint.sh create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/lint.yml diff --git a/.github/actions/docs/Dockerfile b/.github/actions/docs/Dockerfile new file mode 100644 index 000000000..458771951 --- /dev/null +++ b/.github/actions/docs/Dockerfile @@ -0,0 +1,11 @@ +FROM node:12-slim + +LABEL com.github.actions.name="Docs" +LABEL com.github.actions.description="Commit docs to the docs/ branch." +LABEL com.github.actions.icon="upload-cloud" +LABEL com.github.actions.color="blue" + +RUN apt-get update && apt-get install -y git + +COPY src /actions/docs/src +ENTRYPOINT ["/actions/docs/src/entrypoint.sh"] diff --git a/.github/actions/docs/src/entrypoint.sh b/.github/actions/docs/src/entrypoint.sh new file mode 100755 index 000000000..042c166aa --- /dev/null +++ b/.github/actions/docs/src/entrypoint.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -e + +cd $GITHUB_WORKSPACE + +# Run the build +npm run docs +NODE_ENV=production npm run build:browser + +# Initialise some useful variables +REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" +BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` +CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` + +if [ "$BRANCH_OR_TAG" == "heads" ]; then + SOURCE_TYPE="branch" +else + SOURCE_TYPE="tag" +fi + +# Checkout the repo in the target branch so we can build docs and push to it +TARGET_BRANCH="docs" +git clone $REPO out -b $TARGET_BRANCH + +# Move the generated JSON file to the newly-checked-out repo, to be committed and pushed +mv docs/docs.json out/$CURRENT_BRANCH.json + +# Commit and push +cd out +git add . +git config user.name "${GITHUB_ACTOR}" +git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" +git commit -m "Docs build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true +git push origin $TARGET_BRANCH + +# Clean up... +cd .. +rm -rf out + +# ...then do the same once more for the webpack +TARGET_BRANCH="webpack" +git clone $REPO out -b $TARGET_BRANCH + +# Move the generated webpack over +mv webpack/discord.min.js out/discord.$CURRENT_BRANCH.min.js + +# Commit and push +cd out +git add . +git config user.name "${GITHUB_ACTOR}" +git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" +git commit -m "Webpack build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true +git push origin $TARGET_BRANCH diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..5c01c2c75 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,25 @@ +name: Docs + +on: + push: + branches: + - '!gh-action' + - '!webpack' + - '!docs' + +jobs: + docs: + name: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: install node v12 + uses: actions/setup-node@master + with: + node-version: 12 + - name: npm install + run: npm install + - name: deploy docs + uses: ./.github/actions/docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..5f41b66e9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + pull_request: + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: eslint + uses: discordjs/action-eslint@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + job-name: lint + - name: lint typings + run: npm run lint:typings + - name: lint docs + run: npm run docs:test diff --git a/travis/test.sh b/travis/test.sh index bef6134e9..83eb37e10 100644 --- a/travis/test.sh +++ b/travis/test.sh @@ -20,4 +20,4 @@ else fi # Run the tests -npm test +npm run docs:test && npm run lint:typings From 4f9e7f4a239aacc4b38d1a0e908cbe279b311867 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 19:32:40 +0200 Subject: [PATCH 1146/1359] ci: remove travis --- .travis.yml | 19 ---------- travis/deploy-key.enc | Bin 3248 -> 0 bytes travis/deploy.sh | 83 ------------------------------------------ travis/test.sh | 23 ------------ 4 files changed, 125 deletions(-) delete mode 100644 .travis.yml delete mode 100644 travis/deploy-key.enc delete mode 100644 travis/deploy.sh delete mode 100644 travis/test.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2215629ff..000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: node_js -node_js: - - 10 - - 11 -install: npm install -script: bash ./travis/test.sh -jobs: - include: - - stage: deploy - node_js: 10 - script: bash ./travis/deploy.sh - env: - - ENCRYPTION_LABEL="af862fa96d3e" - - COMMIT_AUTHOR_EMAIL="amishshah.2k@gmail.com" -cache: - directories: - - node_modules -dist: trusty -sudo: false diff --git a/travis/deploy-key.enc b/travis/deploy-key.enc deleted file mode 100644 index e03fc36d76feffef7706f7e3b37297e4053dd3fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3248 zcmV;h3{UfQ7qe5cltM)3H)E$<0*dYcb_y)AMS*!fAQBmoURieK{5Ji;@B_GMMkOYb zF}xIvNM3<%jLfQ%!s?X#dB_7qis+rm34dO#srpI5Pc5D+=_qL7%+Qo5{b`624Tk!3@BJYWP!^#wW}gAkJ_V)1hkHNy zjzNG5ERQ5arPF=0x8?`^`auCCoJ6eEQp*vwA6_AW%QlCQ7rQGsWN@373JMgCD{6 zp}XRPZ7-YqJj@g+Z-Mn{#g%Pikv9p|JHO~oTQnkV%;K`zh)IOTqn~ZrZyS0+KGSQz zWC_MqUeYTu*p?PzPD%EAGuUnrWfKfgV~XwnxZU$E-BM&otzBc;~(Am zYaAc|^^twGqRx>+eGCP5E4)59)D;*W@ z&3Pu=lc>Z<7A0Td+X@uko!xe50>5v(>-i`Lkv+*0;B!3jF<6MUXQN$5hxJ(DIwM5k_DiIY`CrOdkbd^@!1n$Thtdgbfm=;J*gyEJ8%#zZi+3 z)Q_X~0WDIwIA0dl1hQXHH2CTLjZvQdi}K`3N5tau{7-2m?u~X(RDqFjT^kz#c0SzC z@+(Xr-}zgA;TH|Ks2YBbzkmeuL?y*j-2|~z_k}VBcil!y#q;b+24)1|2WL*OPk=^Q zo(49b$h3>R#6?ra)GG`Vg3E37*4nzjVX8N=wR!U4KYg=wuh5A8Kn--q?_>GHiD z4D9RD!dRCK5fGX>zd@bV3y=0X$-D6ZX@DbzALQ&LBfr7|+^iEKe^Ud3KOn)h#bF&| zTft%Wfc-NcZ&}j=)IfpxgGM0;6$Se}NI65*RtDF3Gy*a??wrtvx5N-e54u%%)2_DF@`<7%PVo_^i)s z3K!?fdkLX5q^5!e#@?tDzoKdS%ksRZepz1}?@VaSrD8{=;qp~5R4G6{PQU~`EMO@c zjjdB#_hkQK4mo$t#j95*qGhAy5bMEOEl!{+^N1G+`+ zcm(+t%U%YH)UmDgPUsaj7eJhw?#|J@+!`NC>k*X(34r>48V8JYvL>~+x_r-Ar6vW0 zMAfv+qIZ?@ntd_PC&hJ=JDgtFfS~HlHxXexUj1<=hmLT&UYMt*C+k(gA(THdP=yCK zO@hVR3ay8i9%?XPnNW8NmX8pw3I06tEw$%%<$`%o@9g^-rE~d~O-nZ3gsMm)S(G}hvJuVCEc3FE-wcqGw1Rz3S zX|jOU`#e^EYj}e|O3vQ_6GtS!I?TpHROUvI>HB<>(sZIc;HADkftzD4~| zMCq6Tv#`3577YYC@z%gQ1(b*E*;SL~TD=}*Te8lyNu3$}ND%mM);e1gZCR$&A6^aN zRjKLOgt=9Gx=6vzqm&xG9#)lV@YV*HYZGLtxUkDvo6gWu&b4=^pT}$n8Jc7lw^>Lz zH^tZAF{2=zKG-3v6`hGgC(o$e3F!*`e$>z0zMr4gqvns2`FQE4R%a_G|7SZ)O`rv! zwq2W>EZbo^atWCOh;T~B(c+$>tvvc?LtxzVgQk4oMf(7bVLsX+EY=_rlH@bTL zwr)tDG_ClyK-YxS^j)V@>In)w(Ibc|Ap1BfF&*nZDlhpfN`h8Nz~3TA z#J0X#!uTEA3ak_cNFWd;fQvimIiz_Fn45ObH~_Fx)sUPHN?IJjRX-r{HY{M})l4+O z6$Hf^awj!~6^#LV0Ha|CvxU?Ks$tsz1TS9xHf2NM8|x{KKy+AlplB&ec<4!1aGoJy zu^7K@Y;57i6}vzu!jw_lOs`Ec6|7Cdti^m@b^^O}Z!V=wKW()W-_cdzs5gS5Lqij* zUafTBaBDj55HZkoDv}bbWIcI&BdaZ5T`})rS%^30+Tij@2ivsIRm(fIkxmO71$L9! zCXL+`Dc!G+4UqXy^X4kFF(w-of~J*n_iyud@cV|h z!+W6qe_=BJJRy)WXB{7mDBV4RrO%*Z7OLZ+M!}g8x>vqxZ@~=M=$?0Kiaal2L2NUk zOrl9Vl#am2=_Bkz*>+Y90{ccFWEy&9_Enr9ai735V;z1^o3rj?5{{0az-Hd>ss+{N z2I5~47xG-`&7ln2gn|ePl$At`$RhT@kXihGqH0W;RA$>UeNw-=jti4zBdVLZ*G*V?Tq!tbg2}v5L;Ft){qdv;J=1{9R$Z{W3rf$mpJ$Q z%lKVC`sfn>ePjpYV<|Q}axIy0*XoUsCdt8y!t^^f!>DzF5jaoIR zg`dMqg%gSJCB<8blzbsrnS)I-3V~B@hJ_^>h0ucfUX~yFI2V7TL6abO5iW42pI#mv z)r4_>mH9&Q|(#!7WQ_e{;rfw5rak{U=?PE7OeW(HMOG=%Ge)iwTi zZ(v;tzRe3Eo!-ee)YdsC|L0gwDbhVVu5LB3&A*iD5-P)>uhl#fhLhUW8m7Ii^Mniq zQ(NZ_yIm}9QCR&fUfiw<1ap|cgYle8aDM+esvnKAp92B_z8zEkspsNNzIc@&8&vuX zya|*P8DuGz3NN!ppWfRSYaouy%AN=GjbV3@e>!T56sxu|PiXx?0t0-Qk4jPRb>1qav@ z7qckX#}@0JyM0t~-if&7fcwS87-G(!ee3+4hPZt+iqGF=yAf>%XV#DJvdon?h6=4q z_#+~vqW?r+q4l}N+Rf?8^2P7)_^~OX>_0VB Date: Sat, 24 Aug 2019 19:45:52 +0200 Subject: [PATCH 1147/1359] ci: run docs excluding --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5c01c2c75..5775f484f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,6 +3,7 @@ name: Docs on: push: branches: + - '*' - '!gh-action' - '!webpack' - '!docs' From fd49082ac0fd9fcdefdc5f26e79b545570cffa16 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 20:28:44 +0200 Subject: [PATCH 1148/1359] ci: run ci in parallel --- .github/workflows/lint.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5f41b66e9..4d5a78390 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,12 +1,12 @@ -name: Lint +name: ESLint, Typings, and Docs on: push: pull_request: jobs: - lint: - name: lint + eslint: + name: eslint runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -21,8 +21,28 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - job-name: lint + job-name: eslint + + typings-lint: + name: typings-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 - name: lint typings run: npm run lint:typings + + docs-lint: + name: docs-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 - name: lint docs run: npm run docs:test From 57033f3334a0b89167cec2264e125670a7c8a64d Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 20:31:12 +0200 Subject: [PATCH 1149/1359] ci: add npm install steps --- .github/workflows/lint.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4d5a78390..b85aa8b6d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: ESLint, Typings, and Docs +name: Lint on: push: @@ -32,6 +32,8 @@ jobs: uses: actions/setup-node@v1 with: node-version: 12 + - name: npm install + run: npm install - name: lint typings run: npm run lint:typings @@ -44,5 +46,7 @@ jobs: uses: actions/setup-node@v1 with: node-version: 12 + - name: npm install + run: npm install - name: lint docs run: npm run docs:test From 08dfdcb4eb397c9021c22d2c5301ad7bf5d252d1 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 22:17:40 +0200 Subject: [PATCH 1150/1359] ci: deploy docs using new action --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5775f484f..d5f9e18f4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,6 +21,6 @@ jobs: - name: npm install run: npm install - name: deploy docs - uses: ./.github/actions/docs + uses: discordjs/action-docs@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6baff9e3fca155b9af99b724758aa43c86b17e18 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sat, 24 Aug 2019 22:26:08 +0200 Subject: [PATCH 1151/1359] ci: change job name for docs --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d5f9e18f4..9bfbca241 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,7 +9,7 @@ on: - '!docs' jobs: - docs: + deploy: name: deploy runs-on: ubuntu-latest steps: From 8bbf1a922841a64b70265b5bc81d2bd530d54db1 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 25 Aug 2019 07:45:45 +0200 Subject: [PATCH 1152/1359] ci: remove leftover docs action --- .github/actions/docs/Dockerfile | 11 ------ .github/actions/docs/src/entrypoint.sh | 54 -------------------------- 2 files changed, 65 deletions(-) delete mode 100644 .github/actions/docs/Dockerfile delete mode 100755 .github/actions/docs/src/entrypoint.sh diff --git a/.github/actions/docs/Dockerfile b/.github/actions/docs/Dockerfile deleted file mode 100644 index 458771951..000000000 --- a/.github/actions/docs/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:12-slim - -LABEL com.github.actions.name="Docs" -LABEL com.github.actions.description="Commit docs to the docs/ branch." -LABEL com.github.actions.icon="upload-cloud" -LABEL com.github.actions.color="blue" - -RUN apt-get update && apt-get install -y git - -COPY src /actions/docs/src -ENTRYPOINT ["/actions/docs/src/entrypoint.sh"] diff --git a/.github/actions/docs/src/entrypoint.sh b/.github/actions/docs/src/entrypoint.sh deleted file mode 100755 index 042c166aa..000000000 --- a/.github/actions/docs/src/entrypoint.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -set -e - -cd $GITHUB_WORKSPACE - -# Run the build -npm run docs -NODE_ENV=production npm run build:browser - -# Initialise some useful variables -REPO="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" -BRANCH_OR_TAG=`awk -F/ '{print $2}' <<< $GITHUB_REF` -CURRENT_BRANCH=`awk -F/ '{print $NF}' <<< $GITHUB_REF` - -if [ "$BRANCH_OR_TAG" == "heads" ]; then - SOURCE_TYPE="branch" -else - SOURCE_TYPE="tag" -fi - -# Checkout the repo in the target branch so we can build docs and push to it -TARGET_BRANCH="docs" -git clone $REPO out -b $TARGET_BRANCH - -# Move the generated JSON file to the newly-checked-out repo, to be committed and pushed -mv docs/docs.json out/$CURRENT_BRANCH.json - -# Commit and push -cd out -git add . -git config user.name "${GITHUB_ACTOR}" -git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" -git commit -m "Docs build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true -git push origin $TARGET_BRANCH - -# Clean up... -cd .. -rm -rf out - -# ...then do the same once more for the webpack -TARGET_BRANCH="webpack" -git clone $REPO out -b $TARGET_BRANCH - -# Move the generated webpack over -mv webpack/discord.min.js out/discord.$CURRENT_BRANCH.min.js - -# Commit and push -cd out -git add . -git config user.name "${GITHUB_ACTOR}" -git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" -git commit -m "Webpack build for ${SOURCE_TYPE} ${CURRENT_BRANCH}: ${GITHUB_SHA}" || true -git push origin $TARGET_BRANCH From 4c088123026e520f46cb4ee21bb90140d2501215 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Mon, 26 Aug 2019 12:52:30 -0400 Subject: [PATCH 1153/1359] docs(ClientOptions): document unit of restRequestTimeout (#3449) --- src/util/Constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index 50033919d..a344f6536 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -28,7 +28,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * corresponding websocket events * @property {number} [restTimeOffset=500] Extra time in millseconds to wait before continuing to make REST * requests (higher values will reduce rate-limiting errors on bad connections) - * @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request + * @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request, in milliseconds * @property {number} [restSweepInterval=60] How frequently to delete inactive request buckets, in seconds * (or 0 for never) * @property {number} [retryLimit=1] How many times to retry on 5XX errors (Infinity for indefinite amount of retries) From 745a0ea942e277499505e69e7890a1aa768f217a Mon Sep 17 00:00:00 2001 From: didinele Date: Wed, 28 Aug 2019 12:11:15 +0300 Subject: [PATCH 1154/1359] typings(DataStore): correct return types for resolve, resolveID and remove (#3448) * fix(typings): DataStore#resolve & DataStore#resolveID can also return null. * fix(typings): DataStore#remove returns a boolean, not void. Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> --- typings/index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c6a69c911..519acadf9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1715,9 +1715,9 @@ declare module 'discord.js' { public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; - public remove(key: K): void; - public resolve(resolvable: R): V; - public resolveID(resolvable: R): K; + public remove(key: K): boolean; + public resolve(resolvable: R): V | null; + public resolveID(resolvable: R): K | null; } export class GuildEmojiRoleStore extends OverridableDataStore { From 2a3fb705d0296266e3cd7e9976a47459c69653a9 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Wed, 28 Aug 2019 10:13:09 +0100 Subject: [PATCH 1155/1359] fix(ChannelStore): return existing DMChannels within add() (#3438) * fix: return existing DMChannels * ref: group nested conditions --- src/stores/ChannelStore.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index d0e1caa77..762f0e193 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -54,9 +54,9 @@ class ChannelStore extends DataStore { add(data, guild, cache = true) { const existing = this.get(data.id); - if (existing && existing._patch && cache) existing._patch(data); - if (existing && guild) { - guild.channels.add(existing); + if (existing) { + if (existing._patch && cache) existing._patch(data); + if (guild) guild.channels.add(existing); return existing; } From 89d9b0f498b797ab15d7d1452ef0e25a9085fd59 Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 29 Aug 2019 20:16:38 +0200 Subject: [PATCH 1156/1359] fix(typings): partially revert #3448 --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 519acadf9..0e73a24fc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1715,7 +1715,7 @@ declare module 'discord.js' { public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; - public remove(key: K): boolean; + public remove(key: K): void; public resolve(resolvable: R): V | null; public resolveID(resolvable: R): K | null; } From 9e6a73d1a02450a67555adb09c7e0e9daef689ed Mon Sep 17 00:00:00 2001 From: Yukine Date: Sat, 31 Aug 2019 22:17:46 +0200 Subject: [PATCH 1157/1359] typings(Client): remove 'resume', add 'replayed' parameter to 'shardResumed' (#3455) corrected shardResumed & removed old resumed event --- typings/index.d.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0e73a24fc..85d0343a9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -180,7 +180,6 @@ declare module 'discord.js' { public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; - public on(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -192,7 +191,7 @@ declare module 'discord.js' { public on(event: 'shardError', listener: (error: Error, id: number) => void): this; public on(event: 'shardReconnecting', listener: (id: number) => void): this; public on(event: 'shardReady', listener: (id: number) => void): this; - public on(event: 'shardResumed', listener: (id: number) => void): this; + public on(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -218,7 +217,6 @@ declare module 'discord.js' { public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; - public once(event: 'resume', listener: (replayed: number, shardID: number) => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; @@ -230,7 +228,7 @@ declare module 'discord.js' { public once(event: 'shardError', listener: (error: Error, id: number) => void): this; public once(event: 'shardReconnecting', listener: (id: number) => void): this; public once(event: 'shardReady', listener: (id: number) => void): this; - public once(event: 'shardResumed', listener: (id: number) => void): this; + public once(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; public once(event: string, listener: Function): this; } From 4b34f1acbe85aab1dfac85c069bf2a23010ceaa1 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 31 Aug 2019 17:14:48 -0400 Subject: [PATCH 1158/1359] Remove past-tense naming on shard events --- src/client/websocket/WebSocketManager.js | 4 ++-- src/client/websocket/handlers/RESUMED.js | 4 ++-- src/util/Constants.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index c2530ef31..8668d2de5 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -205,11 +205,11 @@ class WebSocketManager extends EventEmitter { if (event.code === 1000 ? this.destroyed : UNRECOVERABLE_CLOSE_CODES.includes(event.code)) { /** * Emitted when a shard's WebSocket disconnects and will no longer reconnect. - * @event Client#shardDisconnected + * @event Client#shardDisconnect * @param {CloseEvent} event The WebSocket close event * @param {number} id The shard ID that disconnected */ - this.client.emit(Events.SHARD_DISCONNECTED, event, shard.id); + this.client.emit(Events.SHARD_DISCONNECT, event, shard.id); this.debug(WSCodes[event.code], shard); return; } diff --git a/src/client/websocket/handlers/RESUMED.js b/src/client/websocket/handlers/RESUMED.js index e345cb74b..5e5f403ac 100644 --- a/src/client/websocket/handlers/RESUMED.js +++ b/src/client/websocket/handlers/RESUMED.js @@ -6,9 +6,9 @@ module.exports = (client, packet, shard) => { const replayed = shard.sequence - shard.closeSequence; /** * Emitted when a shard resumes successfully. - * @event Client#shardResumed + * @event Client#shardResume * @param {number} id The shard ID that resumed * @param {number} replayedEvents The amount of replayed events */ - client.emit(Events.SHARD_RESUMED, shard.id, replayed); + client.emit(Events.SHARD_RESUME, shard.id, replayed); }; diff --git a/src/util/Constants.js b/src/util/Constants.js index a344f6536..235913287 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -264,11 +264,11 @@ exports.Events = { ERROR: 'error', WARN: 'warn', DEBUG: 'debug', - SHARD_DISCONNECTED: 'shardDisconnected', + SHARD_DISCONNECT: 'shardDisconnect', SHARD_ERROR: 'shardError', SHARD_RECONNECTING: 'shardReconnecting', SHARD_READY: 'shardReady', - SHARD_RESUMED: 'shardResumed', + SHARD_RESUME: 'shardResume', INVALIDATED: 'invalidated', RAW: 'raw', }; From 45af62a621ccf41dd26d4f76dabb947abaa55c03 Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Sat, 31 Aug 2019 17:18:18 -0400 Subject: [PATCH 1159/1359] Update typings for renamed shard events --- typings/index.d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 85d0343a9..fc3f2bb1e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -187,11 +187,11 @@ declare module 'discord.js' { public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: 'invalidated', listener: () => void): this; - public on(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public on(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public on(event: 'shardError', listener: (error: Error, id: number) => void): this; public on(event: 'shardReconnecting', listener: (id: number) => void): this; public on(event: 'shardReady', listener: (id: number) => void): this; - public on(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; + public on(event: 'shardResume', listener: (id: number, replayed: number) => void): this; public on(event: string, listener: Function): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; @@ -224,11 +224,11 @@ declare module 'discord.js' { public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: 'invalidated', listener: () => void): this; - public once(event: 'shardDisconnected', listener: (event: CloseEvent, id: number) => void): this; + public once(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public once(event: 'shardError', listener: (error: Error, id: number) => void): this; public once(event: 'shardReconnecting', listener: (id: number) => void): this; public once(event: 'shardReady', listener: (id: number) => void): this; - public once(event: 'shardResumed', listener: (id: number, replayed: number) => void): this; + public once(event: 'shardResume', listener: (id: number, replayed: number) => void): this; public once(event: string, listener: Function): this; } @@ -473,11 +473,11 @@ declare module 'discord.js' { ERROR: 'error'; WARN: 'warn'; DEBUG: 'debug'; - SHARD_DISCONNECTED: 'shardDisconnected'; + SHARD_DISCONNECT: 'shardDisconnect'; SHARD_ERROR: 'shardError'; SHARD_RECONNECTING: 'shardReconnecting'; SHARD_READY: 'shardReady'; - SHARD_RESUMED: 'shardResumed'; + SHARD_RESUME: 'shardResume'; INVALIDATED: 'invalidated'; RAW: 'raw'; }; From 3ce60212e6c92d4bd2d7113682af4a153dbd4d78 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 1 Sep 2019 15:41:07 +0200 Subject: [PATCH 1160/1359] ci: run a lint cronjob every 12h --- .github/workflows/lint-cron.yml | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/lint-cron.yml diff --git a/.github/workflows/lint-cron.yml b/.github/workflows/lint-cron.yml new file mode 100644 index 000000000..077fe4388 --- /dev/null +++ b/.github/workflows/lint-cron.yml @@ -0,0 +1,48 @@ +name: Lint Cronjob + +on: + schedule: + - cron: '0 */12 * * *' + +jobs: + eslint: + name: eslint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: eslint + uses: npm run lint + + typings-lint: + name: typings-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: lint typings + run: npm run lint:typings + + docs-lint: + name: docs-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: npm install + run: npm install + - name: lint docs + run: npm run docs:test From fe71cecc2acd2898364f42c11565980cd3ffd668 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Mon, 2 Sep 2019 13:27:34 +0200 Subject: [PATCH 1161/1359] ci: use the correct command --- .github/workflows/lint-cron.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-cron.yml b/.github/workflows/lint-cron.yml index 077fe4388..2e101d128 100644 --- a/.github/workflows/lint-cron.yml +++ b/.github/workflows/lint-cron.yml @@ -17,7 +17,7 @@ jobs: - name: npm install run: npm install - name: eslint - uses: npm run lint + run: npm run lint typings-lint: name: typings-lint From e7a961781c2a4325ba967d87eed99b11b1f31b2c Mon Sep 17 00:00:00 2001 From: iCrawl Date: Tue, 3 Sep 2019 16:04:36 +0200 Subject: [PATCH 1162/1359] ci: add webpack workflow --- .github/workflows/docs.yml | 1 - .github/workflows/webpack.yml | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/webpack.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9bfbca241..13f197ad1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,7 +4,6 @@ on: push: branches: - '*' - - '!gh-action' - '!webpack' - '!docs' diff --git a/.github/workflows/webpack.yml b/.github/workflows/webpack.yml new file mode 100644 index 000000000..027bfeb37 --- /dev/null +++ b/.github/workflows/webpack.yml @@ -0,0 +1,25 @@ +name: Webpack + +on: + push: + branches: + - '*' + - '!webpack' + - '!docs' + +jobs: + deploy: + name: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: install node v12 + uses: actions/setup-node@master + with: + node-version: 12 + - name: npm install + run: npm install + - name: deploy webpack + uses: discordjs/action-webpack@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0c14616ffc4dc69a9e1b2f9f34339902fe0b0944 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Tue, 3 Sep 2019 16:08:25 +0200 Subject: [PATCH 1163/1359] ci: reduce the amount of yml files --- .github/workflows/{webpack.yml => deploy.yml} | 21 +++++++++++++--- .github/workflows/docs.yml | 25 ------------------- 2 files changed, 18 insertions(+), 28 deletions(-) rename .github/workflows/{webpack.yml => deploy.yml} (51%) delete mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/webpack.yml b/.github/workflows/deploy.yml similarity index 51% rename from .github/workflows/webpack.yml rename to .github/workflows/deploy.yml index 027bfeb37..f1059b2da 100644 --- a/.github/workflows/webpack.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Webpack +name: Deploy on: push: @@ -8,8 +8,23 @@ on: - '!docs' jobs: - deploy: - name: deploy + docs: + name: docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: install node v12 + uses: actions/setup-node@master + with: + node-version: 12 + - name: npm install + run: npm install + - name: docs + uses: discordjs/action-docs@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + webpack: + name: webpack runs-on: ubuntu-latest steps: - uses: actions/checkout@master diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 13f197ad1..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Docs - -on: - push: - branches: - - '*' - - '!webpack' - - '!docs' - -jobs: - deploy: - name: deploy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: install node v12 - uses: actions/setup-node@master - with: - node-version: 12 - - name: npm install - run: npm install - - name: deploy docs - uses: discordjs/action-docs@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0a2003095d4aa045fef6e030ee5dc6d5494214cd Mon Sep 17 00:00:00 2001 From: iCrawl Date: Tue, 3 Sep 2019 16:18:56 +0200 Subject: [PATCH 1164/1359] ci: consistency in naming --- .github/workflows/lint.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b85aa8b6d..63fde90fe 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,8 +23,8 @@ jobs: with: job-name: eslint - typings-lint: - name: typings-lint + typings: + name: typings runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 @@ -37,8 +37,8 @@ jobs: - name: lint typings run: npm run lint:typings - docs-lint: - name: docs-lint + docs: + name: docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 From 5d95a4b264eae333af67ecfceaad1aa63dfdea0e Mon Sep 17 00:00:00 2001 From: BannerBomb Date: Tue, 3 Sep 2019 10:24:20 -0400 Subject: [PATCH 1165/1359] fix: Util#splitMessage when destructured (#3456) --- src/util/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index 8e6743d9f..f0079a2ac 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -56,7 +56,7 @@ class Util { * @returns {string[]} */ static splitMessage(text, { maxLength = 2000, char = '\n', prepend = '', append = '' } = {}) { - text = this.resolveString(text); + text = Util.resolveString(text); if (text.length <= maxLength) return [text]; const splitText = text.split(char); if (splitText.some(chunk => chunk.length > maxLength)) throw new RangeError('SPLIT_MAX_LEN'); From 827cab805e7c7d158a2111bd214ca3f13c613bba Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Wed, 4 Sep 2019 01:02:01 -0400 Subject: [PATCH 1166/1359] Clean up workflows --- .github/workflows/deploy.yml | 67 ++++++++++++++++++--------------- .github/workflows/lint-cron.yml | 48 ----------------------- .github/workflows/lint.yml | 52 ------------------------- .github/workflows/test-cron.yml | 58 ++++++++++++++++++++++++++++ .github/workflows/test.yml | 60 +++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 130 deletions(-) delete mode 100644 .github/workflows/lint-cron.yml delete mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test-cron.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f1059b2da..ceb67cf7f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,40 +1,47 @@ -name: Deploy - +name: Deployment on: push: branches: - - '*' - - '!webpack' - - '!docs' - + - '*' + - '!webpack' + - '!docs' jobs: docs: - name: docs + name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: install node v12 - uses: actions/setup-node@master - with: - node-version: 12 - - name: npm install - run: npm install - - name: docs - uses: discordjs/action-docs@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository + uses: actions/checkout@master + + - name: Install Node v12 + uses: actions/setup-node@master + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Build and deploy documentation + uses: discordjs/action-docs@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + webpack: - name: webpack + name: Webpack runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: install node v12 - uses: actions/setup-node@master - with: - node-version: 12 - - name: npm install - run: npm install - - name: deploy webpack - uses: discordjs/action-webpack@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository + uses: actions/checkout@master + + - name: Install Node v12 + uses: actions/setup-node@master + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Build and deploy Webpack + uses: discordjs/action-webpack@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-cron.yml b/.github/workflows/lint-cron.yml deleted file mode 100644 index 2e101d128..000000000 --- a/.github/workflows/lint-cron.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Lint Cronjob - -on: - schedule: - - cron: '0 */12 * * *' - -jobs: - eslint: - name: eslint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: eslint - run: npm run lint - - typings-lint: - name: typings-lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint typings - run: npm run lint:typings - - docs-lint: - name: docs-lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint docs - run: npm run docs:test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 63fde90fe..000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Lint - -on: - push: - pull_request: - -jobs: - eslint: - name: eslint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: eslint - uses: discordjs/action-eslint@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - job-name: eslint - - typings: - name: typings - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint typings - run: npm run lint:typings - - docs: - name: docs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: npm install - run: npm install - - name: lint docs - run: npm run docs:test diff --git a/.github/workflows/test-cron.yml b/.github/workflows/test-cron.yml new file mode 100644 index 000000000..99ee10670 --- /dev/null +++ b/.github/workflows/test-cron.yml @@ -0,0 +1,58 @@ +name: Testing Cron +on: + schedule: + - cron: '0 */12 * * *' +jobs: + lint: + name: ESLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run ESLint + run: npm run lint + + typings: + name: TSLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run TSLint + run: npm run lint:typings + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Test documentation + run: npm run docs:test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..641a6327b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Testing +on: [push, pull_request] +jobs: + lint: + name: ESLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run ESLint + uses: discordjs/action-eslint@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + job-name: ESLint + + typings: + name: TSLint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Run TSLint + run: npm run lint:typings + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install dependencies + run: npm install + + - name: Test documentation + run: npm run docs:test From 89a3a3a6daa7b5e25bd277b4e2ec6bccb368f1df Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Wed, 4 Sep 2019 01:04:31 -0400 Subject: [PATCH 1167/1359] Lowercase webpack (their branding, not mine) --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ceb67cf7f..4deae7738 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,7 +27,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} webpack: - name: Webpack + name: webpack runs-on: ubuntu-latest steps: - name: Checkout repository @@ -41,7 +41,7 @@ jobs: - name: Install dependencies run: npm install - - name: Build and deploy Webpack + - name: Build and deploy webpack uses: discordjs/action-webpack@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c5cbf8677e72522a366d03c9c90e313b864ff582 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Fri, 6 Sep 2019 12:03:21 +0200 Subject: [PATCH 1168/1359] feat(typings): reply overloads for splitmessage --- typings/index.d.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index fc3f2bb1e..aaf9f524d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -981,8 +981,12 @@ declare module 'discord.js' { public fetch(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; - public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; public toJSON(): object; public toString(): string; public unpin(): Promise; From d252ddf9daa57311b8bc6435a959f2c49f901e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sun, 8 Sep 2019 09:49:10 +0200 Subject: [PATCH 1169/1359] docs: Document Message#author as nullable (#3464) Fixes #3463 --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 99c006b7f..5b6cc4754 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -63,7 +63,7 @@ class Message extends Base { /** * The author of the message - * @type {User} + * @type {?User} */ this.author = data.author ? this.client.users.add(data.author, !data.webhook_id) : null; From c71454f312152129663723ea6a817b6f9800270c Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 8 Sep 2019 11:46:11 +0200 Subject: [PATCH 1170/1359] chore: add more voice files to ignore for webpack --- package.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c234bfb7f..626fea20e 100644 --- a/package.json +++ b/package.json @@ -88,15 +88,20 @@ "src/sharding/ShardClientUtil.js": false, "src/sharding/ShardingManager.js": false, "src/client/voice/ClientVoiceManager.js": false, + "src/client/voice/VoiceBroadcast.js": false, "src/client/voice/VoiceConnection.js": false, + "src/client/voice/dispatcher/BroadcastDispatcher.js": false, + "src/client/voice/dispatcher/StreamDispatcher.js": false, "src/client/voice/networking/VoiceUDPClient.js": false, "src/client/voice/networking/VoiceWebSocket.js": false, - "src/client/voice/dispatcher/StreamDispatcher.js": false, "src/client/voice/player/AudioPlayer.js": false, + "src/client/voice/player/BasePlayer.js": false, + "src/client/voice/player/BroadcastAudioPlayer.js": false, "src/client/voice/receiver/PacketHandler.js": false, "src/client/voice/receiver/Receiver.js": false, + "src/client/voice/util/PlayInterface.js": false, "src/client/voice/util/Secretbox.js": false, - "src/client/voice/util/VolumeInterface.js": false, - "src/client/voice/VoiceBroadcast.js": false + "src/client/voice/util/Silence.js": false, + "src/client/voice/util/VolumeInterface.js": false } } From 6e616edc696d76593f0f19094b5bc38d581ea3dd Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 8 Sep 2019 11:46:47 +0200 Subject: [PATCH 1171/1359] chore: keep_classnames literally everywhere --- webpack.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index c95745f11..f07e0f6a1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,12 +49,13 @@ module.exports = { optimization: { minimizer: [ new TerserJSPlugin({ + cache: false, terserOptions: { mangle: { keep_classnames: true }, compress: { keep_classnames: true }, + keep_classnames: true, output: { comments: false }, }, - parallel: true, }), ], }, From 4fc461c2f932709f51efd65a401d943a7efcec12 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Sun, 8 Sep 2019 11:47:46 +0200 Subject: [PATCH 1172/1359] fix: browser-compatability Fix #3453 --- src/client/websocket/WebSocketShard.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 9663052c6..f77635b0f 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -2,19 +2,24 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); -const { Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); +const { browser, Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); let zstd; let zlib; -try { - zstd = require('zucc'); -} catch (e) { +if (browser) { + zlib = require('pako'); +} else { try { - zlib = require('zlib-sync'); - if (!zlib.Inflate) zlib = require('pako'); - } catch (err) { - zlib = require('pako'); + zstd = require('zucc'); + if (!zstd.DecompressStream) zstd = null; + } catch (e) { + try { + zlib = require('zlib-sync'); + if (!zlib.Inflate) zlib = require('pako'); + } catch (err) { + zlib = require('pako'); + } } } From a6810e2eaaea4021d0a283c7f20ff6d643d34f4f Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Tue, 10 Sep 2019 18:55:42 +1000 Subject: [PATCH 1173/1359] feat(Permissions): add new method Permissions#any (#3450) * Add new method Permissions#any * Update src/util/BitField.js This is much better Co-Authored-By: bdistin * Remove unreachable code * Gotta keep the linter happy * Apply bdistin suggested change to both methods --- src/util/BitField.js | 9 +++++++++ src/util/Permissions.js | 13 +++++++++++-- typings/index.d.ts | 2 ++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/util/BitField.js b/src/util/BitField.js index 424a8443d..757da7ab9 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -17,6 +17,15 @@ class BitField { this.bitfield = this.constructor.resolve(bits); } + /** + * Checks whether the bitfield has a bit, or any of multiple bits. + * @param {BitFieldResolvable} bit Bit(s) to check for + * @returns {boolean} + */ + any(bit) { + return (this.bitfield & this.constructor.resolve(bit)) !== 0; + } + /** * Checks if this bitfield equals another * @param {BitFieldResolvable} bit Bit(s) to check for diff --git a/src/util/Permissions.js b/src/util/Permissions.js index ef5273ffd..831f17386 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -18,6 +18,16 @@ class Permissions extends BitField { * @typedef {string|number|Permissions|PermissionResolvable[]} PermissionResolvable */ + /** + * Checks whether the bitfield has a permission, or any of multiple permissions. + * @param {PermissionResolvable} permission Permission(s) to check for + * @param {boolean} [checkAdmin=true] Whether to allow the administrator permission to override + * @returns {boolean} + */ + any(permission, checkAdmin = true) { + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.any(permission); + } + /** * Checks whether the bitfield has a permission, or multiple permissions. * @param {PermissionResolvable} permission Permission(s) to check for @@ -25,8 +35,7 @@ class Permissions extends BitField { * @returns {boolean} */ has(permission, checkAdmin = true) { - if (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) return true; - return super.has(permission); + return (checkAdmin && super.has(this.constructor.FLAGS.ADMINISTRATOR)) || super.has(permission); } } diff --git a/typings/index.d.ts b/typings/index.d.ts index aaf9f524d..13405c3f0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -99,6 +99,7 @@ declare module 'discord.js' { constructor(bits?: BitFieldResolvable); public bitfield: number; public add(...bits: BitFieldResolvable[]): BitField; + public any(bit: BitFieldResolvable): boolean; public equals(bit: BitFieldResolvable): boolean; public freeze(): Readonly>; public has(bit: BitFieldResolvable): boolean; @@ -1111,6 +1112,7 @@ declare module 'discord.js' { } export class Permissions extends BitField { + public any(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public static ALL: number; From c86a6154aa736aee6bde9fa76ebd34b24225ee79 Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Tue, 10 Sep 2019 02:00:04 -0700 Subject: [PATCH 1174/1359] feat(VoiceState): add kick method (#3462) * feat(VoiceState): add kick method * feat(typings): add types for VoiceState#kick method --- src/structures/VoiceState.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index e2cb2ab99..8e2922a96 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -139,6 +139,15 @@ class VoiceState extends Base { return this.member ? this.member.edit({ deaf }, reason) : Promise.reject(new Error('VOICE_STATE_UNCACHED_MEMBER')); } + /** + * Kicks the member from the voice channel. + * @param {string} [reason] Reason for kicking member from the channel + * @returns {Promise} + */ + kick(reason) { + return this.setChannel(null, reason); + } + /** * Moves the member to a different channel, or kick them from the one they're in. * @param {ChannelResolvable|null} [channel] Channel to move the member to, or `null` if you want to kick them from diff --git a/typings/index.d.ts b/typings/index.d.ts index 13405c3f0..22e40854c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1590,6 +1590,7 @@ declare module 'discord.js' { public setDeaf(deaf: boolean, reason?: string): Promise; public setMute(mute: boolean, reason?: string): Promise; + public kick(reason?: string): Promise; public setChannel(channel: ChannelResolvable | null, reason?: string): Promise; public setSelfDeaf(deaf: boolean): Promise; public setSelfMute(mute: boolean): Promise; From 4072ffb50d28a6918e1b9613f5a4a123e722cf7c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 10 Sep 2019 15:44:00 +0200 Subject: [PATCH 1175/1359] typings(GuildChannel): add members getter (#3467) --- typings/index.d.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 22e40854c..f98eb2305 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -804,6 +804,7 @@ declare module 'discord.js' { public readonly deletable: boolean; public guild: Guild; public readonly manageable: boolean; + public readonly members: Collection; public name: string; public readonly parent: CategoryChannel | null; public parentID: Snowflake; @@ -1362,7 +1363,6 @@ declare module 'discord.js' { export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public readonly members: Collection; public messages: MessageStore; public nsfw: boolean; public rateLimitPerUser: number; @@ -1375,7 +1375,6 @@ declare module 'discord.js' { export class NewsChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public readonly members: Collection; public messages: MessageStore; public nsfw: boolean; public topic: string; @@ -1479,7 +1478,6 @@ declare module 'discord.js' { public readonly editable: boolean; public readonly full: boolean; public readonly joinable: boolean; - public readonly members: Collection; public readonly speakable: boolean; public userLimit: number; public join(): Promise; From 8e0f525d911ce27a8c6c34f0a6fa43107c206ddb Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Tue, 10 Sep 2019 15:44:49 +0200 Subject: [PATCH 1176/1359] fix(Role): throw TypeError in comparePositionTo (#3466) --- src/structures/Role.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Role.js b/src/structures/Role.js index ce9f36373..9c7ebd494 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -150,7 +150,7 @@ class Role extends Base { */ comparePositionTo(role) { role = this.guild.roles.resolve(role); - if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); + if (!role) throw new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake'); return this.constructor.comparePositions(this, role); } From 6f83e715557ce834e0e5021846836698856a9fec Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Tue, 10 Sep 2019 07:47:13 -0600 Subject: [PATCH 1177/1359] feat: Guild#partnered (#3444) * feat: Guild#partnered * typings: added Guild#features * fix: removed trailing space * typings: made Guild#partnered readonly --- src/structures/Guild.js | 9 +++++++++ typings/index.d.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index f24f08e68..9de57145b 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -403,6 +403,15 @@ class Guild extends Base { return new Date(this.joinedTimestamp); } + /** + * If this guild is partnered + * @type {boolean} + * @readonly + */ + get partnered() { + return this.features.includes('PARTNERED'); + } + /** * If this guild is verified * @type {boolean} diff --git a/typings/index.d.ts b/typings/index.d.ts index f98eb2305..af2400847 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -708,6 +708,7 @@ declare module 'discord.js' { public readonly nameAcronym: string; public readonly owner: GuildMember | null; public ownerID: Snowflake; + public readonly partnered: boolean; public premiumSubscriptionCount: number | null; public premiumTier: PremiumTier; public presences: PresenceStore; From b0047c424b5417287d2ea64ed84538931935b698 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Tue, 10 Sep 2019 15:09:06 +0100 Subject: [PATCH 1178/1359] =?UTF-8?q?feat(Partials):=20add=20DMChannel/Mes?= =?UTF-8?q?sageReaction#fetch()=20and=20Parti=E2=80=A6=20(#3261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add DMChannel#fetch() & Action#getChannel({recipients}) * ref for MessageReaction partial * typings * add PartialTypes.REACTION * accommodate for fully removed reactions * fix incorrect wording and typo * typings: MessageReaction#count is nullable * typings: mark MessageReaction#partial as readonly Co-Authored-By: Vlad Frangu * fix(User): fetch dm channel if cached one is partial * docs: add missing comma Co-Authored-By: Antonio Román --- docs/topics/partials.md | 10 ++++--- src/client/actions/Action.js | 5 ++-- src/client/actions/MessageReactionAdd.js | 7 ++--- src/stores/ReactionStore.js | 22 +++++++++++++++ src/structures/DMChannel.js | 10 ++++++- src/structures/MessageReaction.js | 34 +++++++++++++++++++----- src/structures/User.js | 2 +- src/util/Constants.js | 2 ++ typings/index.d.ts | 8 ++++-- 9 files changed, 80 insertions(+), 20 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index 2566f3def..c5c7200da 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -9,8 +9,8 @@ discard the event. With partials, you're able to receive the event, with a Messa Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): ```js -// Accept partial messages and DM channels when emitting events -new Client({ partials: ['MESSAGE', 'CHANNEL'] }); +// Accept partial messages, DM channels, and reactions when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] }); ``` ## Usage & warnings @@ -45,6 +45,10 @@ client.on('messageReactionAdd', async (reaction, user) => { if (reaction.message.partial) await reaction.message.fetch(); // Now the message has been cached and is fully available: console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); + // Fetches and caches the reaction itself, updating resources that were possibly defunct. + if (reaction.partial) await reaction.fetch(); + // Now the reaction is fully available and the properties will be reflected accurately: + console.log(`${reaction.count} user(s) have given the same reaction this message!`); }); ``` @@ -58,4 +62,4 @@ bot or any bot that relies on still receiving updates to resources you don't hav good example. Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should -not be cached. \ No newline at end of file +not be cached. diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 7f21318b4..bc9ed267a 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -36,6 +36,7 @@ class GenericAction { return data.channel || this.getPayload({ id, guild_id: data.guild_id, + recipients: [data.author || { id: data.user_id }], }, this.client.channels, id, PartialTypes.CHANNEL); } @@ -52,9 +53,9 @@ class GenericAction { const id = data.emoji.id || decodeURIComponent(data.emoji.name); return this.getPayload({ emoji: data.emoji, - count: 0, + count: message.partial ? null : 0, me: user.id === this.client.user.id, - }, message.reactions, id, PartialTypes.MESSAGE); + }, message.reactions, id, PartialTypes.REACTION); } getMember(data, guild) { diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index e7ae7e26a..721d9251d 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -26,11 +26,8 @@ class MessageReactionAdd extends Action { if (!message) return false; // Verify reaction - const reaction = message.reactions.add({ - emoji: data.emoji, - count: 0, - me: user.id === this.client.user.id, - }); + const reaction = this.getReaction(data, message, user); + if (!reaction) return false; reaction._add(user); /** * Emitted whenever a reaction is added to a cached message. diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index b3910ba4c..29b53849c 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -50,6 +50,28 @@ class ReactionStore extends DataStore { return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete() .then(() => this.message); } + + _partial(emoji) { + const id = emoji.id || emoji.name; + const existing = this.get(id); + return !existing || existing.partial; + } + + async _fetchReaction(reactionEmoji, cache) { + const id = reactionEmoji.id || reactionEmoji.name; + const existing = this.get(id); + if (!this._partial(reactionEmoji)) return existing; + const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); + if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { + reactionEmoji.reaction._patch({ count: 0 }); + this.message.reactions.remove(id); + return existing; + } + for (const reaction of data.reactions) { + if (this._partial(reaction.emoji)) this.add(reaction, cache); + } + return existing; + } } module.exports = ReactionStore; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index e172f2260..6c034ec92 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -56,7 +56,15 @@ class DMChannel extends Channel { * @readonly */ get partial() { - return !this.recipient; + return this.lastMessageID === undefined; + } + + /** + * Fetch this DMChannel. + * @returns {Promise} + */ + fetch() { + return this.recipient.createDM(); } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index fe10e428f..cd2425952 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -27,12 +27,6 @@ class MessageReaction { */ this.me = data.me; - /** - * The number of people that have given the same reaction - * @type {number} - */ - this.count = data.count || 0; - /** * The users that have given this reaction, mapped by their ID * @type {ReactionUserStore} @@ -40,6 +34,16 @@ class MessageReaction { this.users = new ReactionUserStore(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); + + this._patch(data); + } + + _patch(data) { + /** + * The number of people that have given the same reaction + * @type {?number} + */ + this.count = typeof data.count === 'number' ? data.count : null; } /** @@ -63,18 +67,36 @@ class MessageReaction { return this._emoji; } + /** + * Whether or not this reaction is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.count === null; + } + + /** + * Fetch this reaction. + * @returns {Promise} + */ + fetch() { + return this.message.reactions._fetchReaction(this.emoji, true); + } toJSON() { return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); } _add(user) { + if (this.partial) return; this.users.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { + if (this.partial) return; this.users.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; diff --git a/src/structures/User.js b/src/structures/User.js index c2276c4f2..4425850f8 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -211,7 +211,7 @@ class User extends Base { */ async createDM() { const { dmChannel } = this; - if (dmChannel) return dmChannel; + if (dmChannel && !dmChannel.partial) return dmChannel; const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }); diff --git a/src/util/Constants.js b/src/util/Constants.js index 235913287..89b14c6a0 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -287,6 +287,7 @@ exports.ShardEvents = { * * CHANNEL (only affects DMChannels) * * GUILD_MEMBER * * MESSAGE + * * REACTION * Partials require you to put checks in place when handling data, read the Partials topic listed in the * sidebar for more information. * @typedef {string} PartialType @@ -296,6 +297,7 @@ exports.PartialTypes = keyMirror([ 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', + 'REACTION', ]); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index af2400847..28946efa3 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -653,6 +653,7 @@ declare module 'discord.js' { public messages: MessageStore; public recipient: User; public readonly partial: boolean; + public fetch(): Promise; } export class Emoji extends Base { @@ -1091,11 +1092,13 @@ declare module 'discord.js' { constructor(client: Client, data: object, message: Message); private _emoji: GuildEmoji | ReactionEmoji; - public count: number; + public count: number | null; public readonly emoji: GuildEmoji | ReactionEmoji; public me: boolean; public message: Message; + public readonly partial: boolean; public users: ReactionUserStore; + public fetch(): Promise; public toJSON(): object; } @@ -2452,7 +2455,8 @@ declare module 'discord.js' { type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' - | 'MESSAGE'; + | 'MESSAGE' + | 'REACTION'; type PresenceStatus = ClientPresenceStatus | 'offline'; From 37ecf7b826db002875ff578e31bc9ac001b14882 Mon Sep 17 00:00:00 2001 From: newt Date: Tue, 10 Sep 2019 15:12:27 +0100 Subject: [PATCH 1179/1359] feat(constants): add verificationLevels (#3369) * add Util.parseVerification() * Made the code much cleaner. * Removed method and created constant. * Lint! * refactor(constants): capitalize VerficiationLevels and add a typedef * Changed VerificationLevels typedef to singular. Co-Authored-By: Will Nelson --- src/util/Constants.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 89b14c6a0..ab891eb80 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -467,6 +467,23 @@ exports.Colors = { NOT_QUITE_BLACK: 0x23272A, }; +/** + * The value set for the verification levels for a guild: + * * None + * * Low + * * Medium + * * (╯°□°)╯︵ ┻━┻ + * * ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ + * @typedef {string} VerificationLevel + */ +exports.VerificationLevels = [ + 'None', + 'Low', + 'Medium', + '(╯°□°)╯︵ ┻━┻', + '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', +]; + /** * An error encountered while performing an API request. Here are the potential errors: * * UNKNOWN_ACCOUNT From 8dec251d3c28f1aa273e88085bff772cc56aef53 Mon Sep 17 00:00:00 2001 From: Charalampos Fanoulis Date: Tue, 10 Sep 2019 18:14:07 +0300 Subject: [PATCH 1180/1359] github: add support file and redirect to it (#3302) * github: add support file and redirect to it * github: happily accept that suggestion --- .../ISSUE_TEMPLATE/question---general-support-request.md | 5 +---- .github/SUPPORT.md | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .github/SUPPORT.md diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md index d2cef51f1..d48d49400 100644 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ b/.github/ISSUE_TEMPLATE/question---general-support-request.md @@ -9,8 +9,5 @@ assignees: '' Seriously, we only use this issue tracker for bugs in the library itself and feature requests for it. We don't typically answer questions or help with support issues here. -Instead, please ask in one of the support channels in our Discord server: -https://discord.gg/bRCvFy9 - -Any issues that don't directly involve a bug in the library or a feature request will likely be closed and redirected to the Discord server. +If you have a question or need support on our library, please read our [Support Document](https://github.com/discordjs/discord.js/blob/master/.github/SUPPORT.md) diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 000000000..032cbc2b9 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,7 @@ +# Seeking support? + +We're sorry, we only use this issue tracker for bugs in the library itself and feature requests for it. We are not able to provide general support or answser questions on the issue tracker. + +Should you want to ask such questions, please post in one of our support channels in our Discord server: https://discord.gg/bRCvFy9 + +Any issues that don't directly involve a bug in the library or a feature request will likely be closed and redirected to the Discord server. From 278f185b645353b1f85991cde50e828178e10715 Mon Sep 17 00:00:00 2001 From: bdistin Date: Tue, 10 Sep 2019 10:29:44 -0500 Subject: [PATCH 1181/1359] =?UTF-8?q?fix(rate-limits):=20reactions=20bucke?= =?UTF-8?q?ts=20need=20to=20be=20shared=20with=20sub-=E2=80=A6=20(#3439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rest/APIRouter.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 715b1c7c4..4d1fb5d6b 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -13,13 +13,18 @@ function buildRoute(manager) { get(target, name) { if (reflectors.includes(name)) return () => route.join('/'); if (methods.includes(name)) { + const routeBucket = []; + for (let i = 0; i < route.length; i++) { + // Reactions routes and sub-routes all share the same bucket + if (route[i - 1] === 'reactions') break; + // Literal IDs should only be taken account if they are the Major ID (the Channel/Guild ID) + if (/\d{16,19}/g.test(route[i]) && !/channels|guilds/.test(route[i - 1])) routeBucket.push(':id'); + // All other parts of the route should be considered as part of the bucket identifier + else routeBucket.push(route[i]); + } return options => manager.request(name, route.join('/'), Object.assign({ versioned: manager.versioned, - route: route.map((r, i) => { - if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id'; - if (route[i - 1] === 'reactions') return ':reaction'; - return r; - }).join('/'), + route: routeBucket.join('/'), }, options)); } route.push(name); From dad0cd8e81e3f530d5e0dda1ea45359c602372ca Mon Sep 17 00:00:00 2001 From: 1Computer1 Date: Tue, 10 Sep 2019 11:44:47 -0400 Subject: [PATCH 1182/1359] feat: external collection package (#2934) * Use external collection package * Complete typings * Document properly base collection class * Add clone since sort is now in-place * Update for latest changes to package * Fix whitespace * Update docs link * Oops * Update Collection.js * Update index.d.ts --- package.json | 1 + src/util/Collection.js | 417 +---------------------------------------- src/util/Util.js | 2 +- typings/index.d.ts | 35 +--- 4 files changed, 12 insertions(+), 443 deletions(-) diff --git a/package.json b/package.json index 626fea20e..602973946 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { + "@discordjs/collection": "^0.1.0", "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", diff --git a/src/util/Collection.js b/src/util/Collection.js index 3d01ffef0..13cc0889e 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -1,423 +1,22 @@ 'use strict'; +const BaseCollection = require('@discordjs/collection'); const Util = require('./Util'); /** * A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has * an ID, for significantly improved performance and ease-of-use. - * @extends {Map} + * @extends {BaseCollection} */ -class Collection extends Map { - constructor(iterable) { - super(iterable); - - /** - * Cached array for the `array()` method - will be reset to `null` whenever `set()` or `delete()` are called - * @name Collection#_array - * @type {?Array} - * @private - */ - Object.defineProperty(this, '_array', { value: null, writable: true, configurable: true }); - - /** - * Cached array for the `keyArray()` method - will be reset to `null` whenever `set()` or `delete()` are called - * @name Collection#_keyArray - * @type {?Array} - * @private - */ - Object.defineProperty(this, '_keyArray', { value: null, writable: true, configurable: true }); - } - - set(key, val) { - this._array = null; - this._keyArray = null; - return super.set(key, val); - } - - delete(key) { - this._array = null; - this._keyArray = null; - return super.delete(key); - } - - /** - * Creates an ordered array of the values of this collection, and caches it internally. The array will only be - * reconstructed if an item is added to or removed from the collection, or if you change the length of the array - * itself. If you don't want this caching behavior, use `[...collection.values()]` or - * `Array.from(collection.values())` instead. - * @returns {Array} - */ - array() { - if (!this._array || this._array.length !== this.size) this._array = [...this.values()]; - return this._array; - } - - /** - * Creates an ordered array of the keys of this collection, and caches it internally. The array will only be - * reconstructed if an item is added to or removed from the collection, or if you change the length of the array - * itself. If you don't want this caching behavior, use `[...collection.keys()]` or - * `Array.from(collection.keys())` instead. - * @returns {Array} - */ - keyArray() { - if (!this._keyArray || this._keyArray.length !== this.size) this._keyArray = [...this.keys()]; - return this._keyArray; - } - - /** - * Obtains the first value(s) in this collection. - * @param {number} [amount] Amount of values to obtain from the beginning - * @returns {*|Array<*>} A single value if no amount is provided or an array of values, starting from the end if - * amount is negative - */ - first(amount) { - if (typeof amount === 'undefined') return this.values().next().value; - if (amount < 0) return this.last(amount * -1); - amount = Math.min(this.size, amount); - const arr = new Array(amount); - const iter = this.values(); - for (let i = 0; i < amount; i++) arr[i] = iter.next().value; - return arr; - } - - /** - * Obtains the first key(s) in this collection. - * @param {number} [amount] Amount of keys to obtain from the beginning - * @returns {*|Array<*>} A single key if no amount is provided or an array of keys, starting from the end if - * amount is negative - */ - firstKey(amount) { - if (typeof amount === 'undefined') return this.keys().next().value; - if (amount < 0) return this.lastKey(amount * -1); - amount = Math.min(this.size, amount); - const arr = new Array(amount); - const iter = this.keys(); - for (let i = 0; i < amount; i++) arr[i] = iter.next().value; - return arr; - } - - /** - * Obtains the last value(s) in this collection. This relies on {@link Collection#array}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of values to obtain from the end - * @returns {*|Array<*>} A single value if no amount is provided or an array of values, starting from the start if - * amount is negative - */ - last(amount) { - const arr = this.array(); - if (typeof amount === 'undefined') return arr[arr.length - 1]; - if (amount < 0) return this.first(amount * -1); - if (!amount) return []; - return arr.slice(-amount); - } - - /** - * Obtains the last key(s) in this collection. This relies on {@link Collection#keyArray}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of keys to obtain from the end - * @returns {*|Array<*>} A single key if no amount is provided or an array of keys, starting from the start if - * amount is negative - */ - lastKey(amount) { - const arr = this.keyArray(); - if (typeof amount === 'undefined') return arr[arr.length - 1]; - if (amount < 0) return this.firstKey(amount * -1); - if (!amount) return []; - return arr.slice(-amount); - } - - /** - * Obtains unique random value(s) from this collection. This relies on {@link Collection#array}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of values to obtain randomly - * @returns {*|Array<*>} A single value if no amount is provided or an array of values - */ - random(amount) { - let arr = this.array(); - if (typeof amount === 'undefined') return arr[Math.floor(Math.random() * arr.length)]; - if (arr.length === 0 || !amount) return []; - const rand = new Array(amount); - arr = arr.slice(); - for (let i = 0; i < amount; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; - return rand; - } - - /** - * Obtains unique random key(s) from this collection. This relies on {@link Collection#keyArray}, and thus the caching - * mechanism applies here as well. - * @param {number} [amount] Amount of keys to obtain randomly - * @returns {*|Array<*>} A single key if no amount is provided or an array - */ - randomKey(amount) { - let arr = this.keyArray(); - if (typeof amount === 'undefined') return arr[Math.floor(Math.random() * arr.length)]; - if (arr.length === 0 || !amount) return []; - const rand = new Array(amount); - arr = arr.slice(); - for (let i = 0; i < amount; i++) rand[i] = arr.splice(Math.floor(Math.random() * arr.length), 1)[0]; - return rand; - } - - /** - * Searches for a single item where the given function returns a truthy value. This behaves like - * [Array.find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find). - * All collections used in Discord.js are mapped using their `id` property, and if you want to find by id you - * should use the `get` method. See - * [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get) for details. - * @param {Function} fn The function to test with (should return boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {*} - * @example collection.find(user => user.username === 'Bob'); - */ - find(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (fn(val, key, this)) return val; - } - return undefined; - } - - /* eslint-disable max-len */ - /** - * Searches for the key of a single item where the given function returns a truthy value. This behaves like - * [Array.findIndex()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex), - * but returns the key rather than the positional index. - * @param {Function} fn The function to test with (should return boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {*} - * @example collection.findKey(user => user.username === 'Bob'); - */ - findKey(fn, thisArg) { - /* eslint-enable max-len */ - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (fn(val, key, this)) return key; - } - return undefined; - } - - /** - * Removes entries that satisfy the provided filter function. - * @param {Function} fn Function used to test (should return a boolean) - * @param {Object} [thisArg] Value to use as `this` when executing function - * @returns {number} The number of removed entries - */ - sweep(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const previousSize = this.size; - for (const [key, val] of this) { - if (fn(val, key, this)) this.delete(key); - } - return previousSize - this.size; - } - - /** - * Identical to - * [Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter), - * but returns a Collection instead of an Array. - * @param {Function} fn The function to test with (should return boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection} - * @example collection.filter(user => user.username === 'Bob'); - */ - filter(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = new this.constructor[Symbol.species](); - for (const [key, val] of this) { - if (fn(val, key, this)) results.set(key, val); - } - return results; - } - - /** - * Partitions the collection into two collections where the first collection - * contains the items that passed and the second contains the items that failed. - * @param {Function} fn Function used to test (should return a boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection[]} - * @example const [big, small] = collection.partition(guild => guild.memberCount > 250); - */ - partition(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const results = [new this.constructor[Symbol.species](), new this.constructor[Symbol.species]()]; - for (const [key, val] of this) { - if (fn(val, key, this)) { - results[0].set(key, val); - } else { - results[1].set(key, val); - } - } - return results; - } - - /** - * Maps each item to another value. Identical in behavior to - * [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). - * @param {Function} fn Function that produces an element of the new array, taking three arguments - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Array} - * @example collection.map(user => user.tag); - */ - map(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - const arr = new Array(this.size); - let i = 0; - for (const [key, val] of this) arr[i++] = fn(val, key, this); - return arr; - } - - /** - * Checks if there exists an item that passes a test. Identical in behavior to - * [Array.some()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some). - * @param {Function} fn Function used to test (should return a boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {boolean} - * @example collection.some(user => user.discriminator === '0000'); - */ - some(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (fn(val, key, this)) return true; - } - return false; - } - - /** - * Checks if all items passes a test. Identical in behavior to - * [Array.every()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every). - * @param {Function} fn Function used to test (should return a boolean) - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {boolean} - * @example collection.every(user => !user.bot); - */ - every(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - for (const [key, val] of this) { - if (!fn(val, key, this)) return false; - } - return true; - } - - /** - * Applies a function to produce a single value. Identical in behavior to - * [Array.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce). - * @param {Function} fn Function used to reduce, taking four arguments; `accumulator`, `currentValue`, `currentKey`, - * and `collection` - * @param {*} [initialValue] Starting value for the accumulator - * @returns {*} - * @example collection.reduce((acc, guild) => acc + guild.memberCount, 0); - */ - reduce(fn, initialValue) { - let accumulator; - if (typeof initialValue !== 'undefined') { - accumulator = initialValue; - for (const [key, val] of this) accumulator = fn(accumulator, val, key, this); - } else { - let first = true; - for (const [key, val] of this) { - if (first) { - accumulator = val; - first = false; - continue; - } - accumulator = fn(accumulator, val, key, this); - } - } - return accumulator; - } - - /** - * Identical to - * [Map.forEach()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach), - * but returns the collection instead of undefined. - * @param {Function} fn Function to execute for each element - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection} - * @example - * collection - * .each(user => console.log(user.username)) - * .filter(user => user.bot) - * .each(user => console.log(user.username)); - */ - each(fn, thisArg) { - this.forEach(fn, thisArg); - return this; - } - - /** - * Runs a function on the collection and returns the collection. - * @param {Function} fn Function to execute - * @param {*} [thisArg] Value to use as `this` when executing function - * @returns {Collection} - * @example - * collection - * .tap(coll => console.log(`${coll.size} users, including bots`)) - * .filter(user => user.bot) - * .tap(coll => console.log(`${coll.size} users, excluding bots`)) - */ - tap(fn, thisArg) { - if (typeof thisArg !== 'undefined') fn = fn.bind(thisArg); - fn(this); - return this; - } - - /** - * Creates an identical shallow copy of this collection. - * @returns {Collection} - * @example const newColl = someColl.clone(); - */ - clone() { - return new this.constructor[Symbol.species](this); - } - - /** - * Combines this collection with others into a new collection. None of the source collections are modified. - * @param {...Collection} collections Collections to merge - * @returns {Collection} - * @example const newColl = someColl.concat(someOtherColl, anotherColl, ohBoyAColl); - */ - concat(...collections) { - const newColl = this.clone(); - for (const coll of collections) { - for (const [key, val] of coll) newColl.set(key, val); - } - return newColl; - } - - /** - * Checks if this collection shares identical key-value pairings with another. - * This is different to checking for equality using equal-signs, because - * the collections may be different objects, but contain the same data. - * @param {Collection} collection Collection to compare with - * @returns {boolean} Whether the collections have identical contents - */ - equals(collection) { - if (!collection) return false; - if (this === collection) return true; - if (this.size !== collection.size) return false; - return !this.find((value, key) => { - const testVal = collection.get(key); - return testVal !== value || (testVal === undefined && !collection.has(key)); - }); - } - - /** - * The sort() method sorts the elements of a collection and returns it. - * The sort is not necessarily stable. The default sort order is according to string Unicode code points. - * @param {Function} [compareFunction] Specifies a function that defines the sort order. - * If omitted, the collection is sorted according to each character's Unicode code point value, - * according to the string conversion of each element. - * @returns {Collection} - * @example collection.sort((userA, userB) => userA.createdTimestamp - userB.createdTimestamp); - */ - sort(compareFunction = (x, y) => +(x > y) || +(x === y) - 1) { - return new this.constructor[Symbol.species]([...this.entries()] - .sort((a, b) => compareFunction(a[1], b[1], a[0], b[0]))); - } - +class Collection extends BaseCollection { toJSON() { return this.map(e => typeof e.toJSON === 'function' ? e.toJSON() : Util.flatten(e)); } } module.exports = Collection; + +/** + * @external BaseCollection + * @see {@link https://discord.js.org/#/docs/collection/} + */ diff --git a/src/util/Util.js b/src/util/Util.js index f0079a2ac..6087f3c24 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -432,7 +432,7 @@ class Util { * @returns {Collection} */ static discordSort(collection) { - return collection.sort((a, b) => + return collection.sorted((a, b) => a.rawPosition - b.rawPosition || parseInt(b.id.slice(0, -10)) - parseInt(a.id.slice(0, -10)) || parseInt(b.id.slice(10)) - parseInt(a.id.slice(10)) diff --git a/typings/index.d.ts b/typings/index.d.ts index 28946efa3..4a6896c1e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,4 +1,5 @@ declare module 'discord.js' { + import BaseCollection from '@discord.js/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; @@ -311,39 +312,7 @@ declare module 'discord.js' { public setUsername(username: string): Promise; } - export class Collection extends Map { - private _array: V[]; - private _keyArray: K[]; - - public array(): V[]; - public clone(): Collection; - public concat(...collections: Collection[]): Collection; - public each(fn: (value: V, key: K, collection: Collection) => void, thisArg?: any): Collection; - public equals(collection: Collection): boolean; - public every(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; - public filter(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): Collection; - public find(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): V | undefined; - public findKey(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): K | undefined; - public first(): V | undefined; - public first(count: number): V[]; - public firstKey(): K | undefined; - public firstKey(count: number): K[]; - public keyArray(): K[]; - public last(): V | undefined; - public last(count: number): V[]; - public lastKey(): K | undefined; - public lastKey(count: number): K[]; - public map(fn: (value: V, key: K, collection: Collection) => T, thisArg?: any): T[]; - public partition(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): [Collection, Collection]; - public random(): V | undefined; - public random(count: number): V[]; - public randomKey(): K | undefined; - public randomKey(count: number): K[]; - public reduce(fn: (accumulator: T, value: V, key: K, collection: Collection) => T, initialValue?: T): T; - public some(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): boolean; - public sort(compareFunction?: (a: V, b: V, c?: K, d?: K) => number): Collection; - public sweep(fn: (value: V, key: K, collection: Collection) => boolean, thisArg?: any): number; - public tap(fn: (collection: Collection) => void, thisArg?: any): Collection; + export class Collection extends BaseCollection { public toJSON(): object; } From 321beb73bd92c939fae701cde774c238a98b613a Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 10 Sep 2019 19:49:56 +0200 Subject: [PATCH 1183/1359] =?UTF-8?q?revert:=20"feat(Partials):=20add=20DM?= =?UTF-8?q?Channel/MessageReaction#fetch()=E2=80=A6=20(#3468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b0047c424b5417287d2ea64ed84538931935b698. --- docs/topics/partials.md | 10 +++---- src/client/actions/Action.js | 5 ++-- src/client/actions/MessageReactionAdd.js | 7 +++-- src/stores/ReactionStore.js | 22 --------------- src/structures/DMChannel.js | 10 +------ src/structures/MessageReaction.js | 34 +++++------------------- src/structures/User.js | 2 +- src/util/Constants.js | 2 -- typings/index.d.ts | 8 ++---- 9 files changed, 20 insertions(+), 80 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index c5c7200da..2566f3def 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -9,8 +9,8 @@ discard the event. With partials, you're able to receive the event, with a Messa Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): ```js -// Accept partial messages, DM channels, and reactions when emitting events -new Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] }); +// Accept partial messages and DM channels when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL'] }); ``` ## Usage & warnings @@ -45,10 +45,6 @@ client.on('messageReactionAdd', async (reaction, user) => { if (reaction.message.partial) await reaction.message.fetch(); // Now the message has been cached and is fully available: console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); - // Fetches and caches the reaction itself, updating resources that were possibly defunct. - if (reaction.partial) await reaction.fetch(); - // Now the reaction is fully available and the properties will be reflected accurately: - console.log(`${reaction.count} user(s) have given the same reaction this message!`); }); ``` @@ -62,4 +58,4 @@ bot or any bot that relies on still receiving updates to resources you don't hav good example. Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should -not be cached. +not be cached. \ No newline at end of file diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index bc9ed267a..7f21318b4 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -36,7 +36,6 @@ class GenericAction { return data.channel || this.getPayload({ id, guild_id: data.guild_id, - recipients: [data.author || { id: data.user_id }], }, this.client.channels, id, PartialTypes.CHANNEL); } @@ -53,9 +52,9 @@ class GenericAction { const id = data.emoji.id || decodeURIComponent(data.emoji.name); return this.getPayload({ emoji: data.emoji, - count: message.partial ? null : 0, + count: 0, me: user.id === this.client.user.id, - }, message.reactions, id, PartialTypes.REACTION); + }, message.reactions, id, PartialTypes.MESSAGE); } getMember(data, guild) { diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index 721d9251d..e7ae7e26a 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -26,8 +26,11 @@ class MessageReactionAdd extends Action { if (!message) return false; // Verify reaction - const reaction = this.getReaction(data, message, user); - if (!reaction) return false; + const reaction = message.reactions.add({ + emoji: data.emoji, + count: 0, + me: user.id === this.client.user.id, + }); reaction._add(user); /** * Emitted whenever a reaction is added to a cached message. diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 29b53849c..b3910ba4c 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -50,28 +50,6 @@ class ReactionStore extends DataStore { return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete() .then(() => this.message); } - - _partial(emoji) { - const id = emoji.id || emoji.name; - const existing = this.get(id); - return !existing || existing.partial; - } - - async _fetchReaction(reactionEmoji, cache) { - const id = reactionEmoji.id || reactionEmoji.name; - const existing = this.get(id); - if (!this._partial(reactionEmoji)) return existing; - const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); - if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { - reactionEmoji.reaction._patch({ count: 0 }); - this.message.reactions.remove(id); - return existing; - } - for (const reaction of data.reactions) { - if (this._partial(reaction.emoji)) this.add(reaction, cache); - } - return existing; - } } module.exports = ReactionStore; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 6c034ec92..e172f2260 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -56,15 +56,7 @@ class DMChannel extends Channel { * @readonly */ get partial() { - return this.lastMessageID === undefined; - } - - /** - * Fetch this DMChannel. - * @returns {Promise} - */ - fetch() { - return this.recipient.createDM(); + return !this.recipient; } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index cd2425952..fe10e428f 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -27,6 +27,12 @@ class MessageReaction { */ this.me = data.me; + /** + * The number of people that have given the same reaction + * @type {number} + */ + this.count = data.count || 0; + /** * The users that have given this reaction, mapped by their ID * @type {ReactionUserStore} @@ -34,16 +40,6 @@ class MessageReaction { this.users = new ReactionUserStore(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); - - this._patch(data); - } - - _patch(data) { - /** - * The number of people that have given the same reaction - * @type {?number} - */ - this.count = typeof data.count === 'number' ? data.count : null; } /** @@ -67,36 +63,18 @@ class MessageReaction { return this._emoji; } - /** - * Whether or not this reaction is a partial - * @type {boolean} - * @readonly - */ - get partial() { - return this.count === null; - } - - /** - * Fetch this reaction. - * @returns {Promise} - */ - fetch() { - return this.message.reactions._fetchReaction(this.emoji, true); - } toJSON() { return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); } _add(user) { - if (this.partial) return; this.users.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { - if (this.partial) return; this.users.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; diff --git a/src/structures/User.js b/src/structures/User.js index 4425850f8..c2276c4f2 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -211,7 +211,7 @@ class User extends Base { */ async createDM() { const { dmChannel } = this; - if (dmChannel && !dmChannel.partial) return dmChannel; + if (dmChannel) return dmChannel; const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }); diff --git a/src/util/Constants.js b/src/util/Constants.js index ab891eb80..9d742e784 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -287,7 +287,6 @@ exports.ShardEvents = { * * CHANNEL (only affects DMChannels) * * GUILD_MEMBER * * MESSAGE - * * REACTION * Partials require you to put checks in place when handling data, read the Partials topic listed in the * sidebar for more information. * @typedef {string} PartialType @@ -297,7 +296,6 @@ exports.PartialTypes = keyMirror([ 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', - 'REACTION', ]); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 4a6896c1e..e56769f38 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -622,7 +622,6 @@ declare module 'discord.js' { public messages: MessageStore; public recipient: User; public readonly partial: boolean; - public fetch(): Promise; } export class Emoji extends Base { @@ -1061,13 +1060,11 @@ declare module 'discord.js' { constructor(client: Client, data: object, message: Message); private _emoji: GuildEmoji | ReactionEmoji; - public count: number | null; + public count: number; public readonly emoji: GuildEmoji | ReactionEmoji; public me: boolean; public message: Message; - public readonly partial: boolean; public users: ReactionUserStore; - public fetch(): Promise; public toJSON(): object; } @@ -2424,8 +2421,7 @@ declare module 'discord.js' { type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' - | 'MESSAGE' - | 'REACTION'; + | 'MESSAGE'; type PresenceStatus = ClientPresenceStatus | 'offline'; From ac44a7fc5717e4dfad52285658e2c529fcc60875 Mon Sep 17 00:00:00 2001 From: Crawl Date: Tue, 10 Sep 2019 20:34:47 +0200 Subject: [PATCH 1184/1359] fix(typings): collections import --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e56769f38..501d54e9a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,5 @@ declare module 'discord.js' { - import BaseCollection from '@discord.js/collection'; + import BaseCollection from '@discordjs/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; From 33ecdd4c5039bc2e2c50f8f5a24783e2df21e4f7 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Wed, 11 Sep 2019 01:58:12 +0200 Subject: [PATCH 1185/1359] fix(typings): collection constructor --- package.json | 2 +- typings/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 602973946..e89ca4fc3 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { - "@discordjs/collection": "^0.1.0", + "@discordjs/collection": "^0.1.1", "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", diff --git a/typings/index.d.ts b/typings/index.d.ts index 501d54e9a..c694fa260 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,5 @@ declare module 'discord.js' { - import BaseCollection from '@discordjs/collection'; + import BaseCollection, { CollectionConstructor } from '@discordjs/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; @@ -1684,7 +1684,7 @@ declare module 'discord.js' { export class DataStore, R = any> extends Collection { constructor(client: Client, iterable: Iterable, holds: VConstructor); - public static readonly [Symbol.species]: typeof Collection; + public static readonly [Symbol.species]: typeof CollectionConstructor; public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; From ea9e1441905e67e2627a6f5e80a8d989d01376c5 Mon Sep 17 00:00:00 2001 From: iCrawl Date: Wed, 11 Sep 2019 02:11:31 +0200 Subject: [PATCH 1186/1359] fix(typings): remove leftover typeof --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c694fa260..d71dde4d7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1684,7 +1684,7 @@ declare module 'discord.js' { export class DataStore, R = any> extends Collection { constructor(client: Client, iterable: Iterable, holds: VConstructor); - public static readonly [Symbol.species]: typeof CollectionConstructor; + public static readonly [Symbol.species]: CollectionConstructor; public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; From d5831df7b18e3b71c9ad7057b85fc3c84aa7c609 Mon Sep 17 00:00:00 2001 From: 1Computer1 Date: Tue, 17 Sep 2019 18:53:00 -0400 Subject: [PATCH 1187/1359] fix: typings for this-polymorphism of collections (#3472) * Fix typings for this-polymorphism of collections * Update index.d.ts Co-authored-by: Crawl --- typings/index.d.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index d71dde4d7..811aa78ae 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,5 @@ declare module 'discord.js' { - import BaseCollection, { CollectionConstructor } from '@discordjs/collection'; + import BaseCollection from '@discordjs/collection'; import { EventEmitter } from 'events'; import { Stream, Readable, Writable } from 'stream'; import { ChildProcess } from 'child_process'; @@ -313,6 +313,12 @@ declare module 'discord.js' { } export class Collection extends BaseCollection { + public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; + public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; + public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; + public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; public toJSON(): object; } @@ -1684,13 +1690,37 @@ declare module 'discord.js' { export class DataStore, R = any> extends Collection { constructor(client: Client, iterable: Iterable, holds: VConstructor); - public static readonly [Symbol.species]: CollectionConstructor; public client: Client; public holds: VConstructor; public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; public remove(key: K): void; public resolve(resolvable: R): V | null; public resolveID(resolvable: R): K | null; + // Don't worry about those bunch of ts-ignores here, this is intended https://github.com/microsoft/TypeScript/issues/1213 + // @ts-ignore + public filter(fn: (value: V, key: K, collection: this) => boolean): Collection; + // @ts-ignore + public filter(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): Collection; + // @ts-ignore + public filter(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): Collection; + // @ts-ignore + public partition(fn: (value: V, key: K, collection: this) => boolean): [Collection, Collection]; + // @ts-ignore + public partition(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): [Collection, Collection]; + // @ts-ignore + public partition(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): [Collection, Collection]; + public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; + public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; + public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; + public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; + public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; + // @ts-ignore + public clone(): Collection; + // @ts-ignore + public concat(...collections: Collection[]): Collection; + // @ts-ignore + public sorted(compareFunction: (firstValue: V, secondValue: V, firstKey: K, secondKey: K) => number): Collection; } export class GuildEmojiRoleStore extends OverridableDataStore { From d05334fd3cd8fb913e869654f25eb8230493de11 Mon Sep 17 00:00:00 2001 From: BadCoder1337 Date: Fri, 20 Sep 2019 00:01:08 +0300 Subject: [PATCH 1188/1359] typings: make TypeScript interaction with channels better (#3469) * make channel returning methods generic * channel's type inference from kind of channel * add extends * rename generic due to name convention * edit overload * Update index.d.ts * Update index.d.ts * Update index.d.ts Co-authored-by: Crawl Co-authored-by: SpaceEEC --- typings/index.d.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 811aa78ae..fdf1a5b81 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -789,20 +789,20 @@ declare module 'discord.js' { public readonly position: number; public rawPosition: number; public readonly viewable: boolean; - public clone(options?: GuildChannelCloneOptions): Promise; + public clone(options?: GuildChannelCloneOptions): Promise; public createInvite(options?: InviteOptions): Promise; - public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; - public edit(data: ChannelData, reason?: string): Promise; + public createOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; + public edit(data: ChannelData, reason?: string): Promise; public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; - public lockPermissions(): Promise; - public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; + public lockPermissions(): Promise; + public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; - public setName(name: string, reason?: string): Promise; - public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; - public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; - public setTopic(topic: string, reason?: string): Promise; - public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; + public setName(name: string, reason?: string): Promise; + public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; + public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; + public setTopic(topic: string, reason?: string): Promise; + public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } export class StoreChannel extends GuildChannel { @@ -1738,7 +1738,10 @@ declare module 'discord.js' { export class GuildChannelStore extends DataStore { constructor(guild: Guild, iterable?: Iterable); - public create(name: string, options?: GuildCreateChannelOptions): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'voice' }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'category' }): Promise; + public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text' }): Promise; + public create(name: string, options: GuildCreateChannelOptions): Promise; } // Hacky workaround because changing the signature of an overridden method errors From 60f89bd96f7e110617af021642a810e7fb71e694 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Mon, 23 Sep 2019 07:50:41 +1000 Subject: [PATCH 1189/1359] fix(stores): Add symbol.species for not-actual-stores (#3477) * fix(stores): Add symbol.species for not-actual-stores * Linting fixes --- src/stores/GuildEmojiRoleStore.js | 4 ++++ src/stores/GuildMemberRoleStore.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/stores/GuildEmojiRoleStore.js b/src/stores/GuildEmojiRoleStore.js index ad3793b29..6db2003ed 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/stores/GuildEmojiRoleStore.js @@ -105,6 +105,10 @@ class GuildEmojiRoleStore extends Collection { valueOf() { return this._filtered; } + + static get [Symbol.species]() { + return Collection; + } } Util.mixin(GuildEmojiRoleStore, ['set']); diff --git a/src/stores/GuildMemberRoleStore.js b/src/stores/GuildMemberRoleStore.js index 4c63bed83..047b8086a 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/stores/GuildMemberRoleStore.js @@ -154,6 +154,10 @@ class GuildMemberRoleStore extends Collection { valueOf() { return this._filtered; } + + static get [Symbol.species]() { + return Collection; + } } Util.mixin(GuildMemberRoleStore, ['set']); From d280d1b03f595f1c4464ab1a8ba20cf74088ec2d Mon Sep 17 00:00:00 2001 From: Gryffon Bellish <39341355+PyroTechniac@users.noreply.github.com> Date: Tue, 24 Sep 2019 11:36:19 -0400 Subject: [PATCH 1190/1359] style: change let to const in MessageMentions (#3483) --- src/structures/MessageMentions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index b4414a3bb..610c8c127 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -47,7 +47,7 @@ class MessageMentions { } else { this.users = new Collection(); for (const mention of users) { - let user = message.client.users.add(mention); + const user = message.client.users.add(mention); this.users.set(user.id, user); } } From ea8b4e73559d118a826ac3b242c1a3626b4f6d29 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 26 Sep 2019 00:28:12 +0300 Subject: [PATCH 1191/1359] docs/typings: Rename LURKABLE to PUBLIC and update GuildFeatures type (#3484) * docs: Rename LURKABLE to PUBLIC * typings: Update GuildFeatures type --- src/structures/Guild.js | 2 +- typings/index.d.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9de57145b..75925663e 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -154,7 +154,7 @@ class Guild extends Base { * * DISCOVERABLE * * FEATURABLE * * INVITE_SPLASH - * * LURKABLE + * * PUBLIC * * NEWS * * PARTNERED * * VANITY_URL diff --git a/typings/index.d.ts b/typings/index.d.ts index fdf1a5b81..4f03ef11d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2235,13 +2235,18 @@ declare module 'discord.js' { channel: GuildChannelResolvable | null; } - type GuildFeatures = 'INVITE_SPLASH' - | 'MORE_EMOJI' - | 'VERIFIED' - | 'VIP_REGIONS' - | 'VANITY_URL' + type GuildFeatures = 'ANIMATED_ICON' + | 'BANNER' + | 'COMMERCE' | 'DISCOVERABLE' - | 'FEATURABLE'; + | 'FEATURABLE' + | 'INVITE_SPLASH' + | 'PUBLIC' + | 'NEWS' + | 'PARTNERED' + | 'VANITY_URL' + | 'VERIFIED' + | 'VIP_REGIONS'; interface GuildMemberEditData { nick?: string; From 41c0dd44eb53c8048c40e9ab5be5530d6330fb81 Mon Sep 17 00:00:00 2001 From: Jiralite <33201955+Jiralite@users.noreply.github.com> Date: Tue, 1 Oct 2019 09:46:49 +0100 Subject: [PATCH 1192/1359] fix(BitField): throw when resolving invalid string constant Checked to see if the permission actually exists. --- src/util/BitField.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/BitField.js b/src/util/BitField.js index 757da7ab9..f849e7b8a 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -146,7 +146,7 @@ class BitField { if (typeof bit === 'number' && bit >= 0) return bit; if (bit instanceof BitField) return bit.bitfield; if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, 0); - if (typeof bit === 'string') return this.FLAGS[bit]; + if (typeof bit === 'string' && typeof this.FLAGS[bit] !== 'undefined') return this.FLAGS[bit]; throw new RangeError('BITFIELD_INVALID'); } } From a03e439d6b5459aaef8671c34f20f193e1248208 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Tue, 1 Oct 2019 18:56:14 +1000 Subject: [PATCH 1193/1359] fix(GuildChannelStore): default channel type incorrectly set (#3496) --- src/stores/GuildChannelStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index 55b9ebb0c..552b40f00 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -102,7 +102,7 @@ class GuildChannelStore extends DataStore { data: { name, topic, - type: type ? ChannelTypes[type.toUpperCase()] : 'text', + type: type ? ChannelTypes[type.toUpperCase()] : ChannelTypes.TEXT, nsfw, bitrate, user_limit: userLimit, From a4f06bdffd12e4478ba2862bad8c178839e9fa21 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 1 Oct 2019 12:01:55 +0300 Subject: [PATCH 1194/1359] src: support new message fields (#3388) * src: Update channel pattern * src: Remove useless non-capture group * src: it's as though we're starting fresh * src: Bring this up to date for reals now * src: typings and a bug fix * src: Add crossposted channels to message mentions * src: Requested changes and add typings * src: Move Object.keys outside loop * typings: Fix enum being exported when it shouldn't * src: Consistency with roles and users * docs: Correct docstring for MessageFlags#flags * docs: Correct docstring for MessageMentions#crosspostedChannels * docs: Suggestions Co-authored-by: SpaceEEC * src: Reset flags to 0 if no flags are received on MESSAGE_UPDATE --- src/index.js | 1 + src/structures/Message.js | 38 +++++++++++++++++++++--- src/structures/MessageMentions.js | 36 ++++++++++++++++++++++- src/util/Constants.js | 2 ++ src/util/MessageFlags.js | 25 ++++++++++++++++ typings/index.d.ts | 49 +++++++++++++++++++++++++++---- 6 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 src/util/MessageFlags.js diff --git a/src/index.js b/src/index.js index 916056fe0..77c56336d 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ module.exports = { DataStore: require('./stores/DataStore'), DiscordAPIError: require('./rest/DiscordAPIError'), HTTPError: require('./rest/HTTPError'), + MessageFlags: require('./util/MessageFlags'), Permissions: require('./util/Permissions'), Speaking: require('./util/Speaking'), Snowflake: require('./util/Snowflake'), diff --git a/src/structures/Message.js b/src/structures/Message.js index 5b6cc4754..8a9df07ea 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -13,6 +13,7 @@ const Permissions = require('../util/Permissions'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); const APIMessage = require('./APIMessage'); +const MessageFlags = require('../util/MessageFlags'); /** * Represents a message on Discord. @@ -81,7 +82,9 @@ class Message extends Base { /** * A random number or string used for checking message delivery - * @type {string} + * This is only received after the message was sent successfully, and + * lost if re-fetched + * @type {?string} */ this.nonce = data.nonce; @@ -89,7 +92,7 @@ class Message extends Base { * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) * @type {boolean} */ - this.system = data.type === 6; + this.system = data.type !== 0; /** * A list of embeds in the message - e.g. YouTube Player @@ -137,7 +140,7 @@ class Message extends Base { * All valid mentions that the message contains * @type {MessageMentions} */ - this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone); + this.mentions = new Mentions(this, data.mentions, data.mention_roles, data.mention_everyone, data.mention_channels); /** * ID of the webhook that sent the message, if applicable @@ -172,6 +175,30 @@ class Message extends Base { } else if (data.member && this.guild && this.author) { this.guild.members.add(Object.assign(data.member, { user: this.author })); } + + /** + * Flags that are applied to the message + * @type {Readonly} + */ + this.flags = new MessageFlags(data.flags).freeze(); + + /** + * Reference data sent in a crossposted message. + * @typedef {Object} MessageReference + * @property {string} channelID ID of the channel the message was crossposted from + * @property {?string} guildID ID of the guild the message was crossposted from + * @property {?string} messageID ID of the message that was crossposted + */ + + /** + * Message reference data + * @type {?MessageReference} + */ + this.reference = data.message_reference ? { + channelID: data.message_reference.channel_id, + guildID: data.message_reference.guild_id, + messageID: data.message_reference.message_id, + } : null; } /** @@ -214,8 +241,11 @@ class Message extends Base { this, 'mentions' in data ? data.mentions : this.mentions.users, 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, - 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone + 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, + 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels ); + + this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); } /** diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 610c8c127..9b17685f6 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -3,12 +3,13 @@ const Collection = require('../util/Collection'); const Util = require('../util/Util'); const GuildMember = require('./GuildMember'); +const { ChannelTypes } = require('../util/Constants'); /** * Keeps track of mentions in a {@link Message}. */ class MessageMentions { - constructor(message, users, roles, everyone) { + constructor(message, users, roles, everyone, crosspostedChannels) { /** * The client the message is from * @type {Client} @@ -86,6 +87,39 @@ class MessageMentions { * @private */ this._channels = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {string} channelID ID of the mentioned channel + * @property {string} guildID ID of the guild that has the channel + * @property {string} type Type of the channel + * @property {string} name The name of the channel + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * @type {Collection} + */ + this.crosspostedChannels = new Collection(crosspostedChannels); + } else { + this.crosspostedChannels = new Collection(); + const channelTypes = Object.keys(ChannelTypes); + for (const d of crosspostedChannels) { + const type = channelTypes[d.type]; + this.crosspostedChannels.set(d.id, { + channelID: d.id, + guildID: d.guild_id, + type: type ? type.toLowerCase() : 'unknown', + name: d.name, + }); + } + } + } else { + this.crosspostedChannels = new Collection(); + } } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 9d742e784..7bb3bb50e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -386,6 +386,7 @@ exports.WSEvents = keyMirror([ * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 + * * CHANNEL_FOLLOW_ADD * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -401,6 +402,7 @@ exports.MessageTypes = [ 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', + 'CHANNEL_FOLLOW_ADD', ]; /** diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js new file mode 100644 index 000000000..b1c86b7d6 --- /dev/null +++ b/src/util/MessageFlags.js @@ -0,0 +1,25 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link Message#flags} bitfield. + * @extends {BitField} + */ +class MessageFlags extends BitField {} + +/** + * Numeric message flags. All available properties: + * * `CROSSPOSTED` + * * `IS_CROSSPOST` + * * `SUPPRESS_EMBEDS` + * @type {Object} + * @see {@link https://discordapp.com/developers/docs/resources/channel#message-object-message-flags} + */ +MessageFlags.FLAGS = { + CROSSPOSTED: 1 << 0, + IS_CROSSPOST: 1 << 1, + SUPPRESS_EMBEDS: 1 << 2, +}; + +module.exports = MessageFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index 4f03ef11d..cb08e2ffe 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,3 +1,14 @@ +declare enum ChannelType { + text, + dm, + voice, + group, + category, + news, + store, + unknown +} + declare module 'discord.js' { import BaseCollection from '@discordjs/collection'; import { EventEmitter } from 'events'; @@ -125,7 +136,7 @@ declare module 'discord.js' { public readonly createdTimestamp: number; public deleted: boolean; public id: Snowflake; - public type: 'dm' | 'text' | 'voice' | 'category' | 'news' | 'store' | 'unknown'; + public type: keyof typeof ChannelType; public delete(reason?: string): Promise; public fetch(): Promise; public toString(): string; @@ -914,12 +925,17 @@ declare module 'discord.js' { public toString(): string; } + export class MessageFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + export class Message extends Base { constructor(client: Client, data: object, channel: TextChannel | DMChannel); private _edits: Message[]; private patch(data: object): void; - public activity: GroupActivity | null; + public activity: MessageActivity | null; public application: ClientApplication | null; public attachments: Collection; public author: User | null; @@ -939,7 +955,7 @@ declare module 'discord.js' { public id: Snowflake; public readonly member: GuildMember | null; public mentions: MessageMentions; - public nonce: string; + public nonce: string | null; public readonly partial: boolean; public readonly pinnable: boolean; public pinned: boolean; @@ -949,6 +965,8 @@ declare module 'discord.js' { public type: MessageType; public readonly url: string; public webhookID: Snowflake | null; + public flags: Readonly; + public reference: MessageReference | null; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; public delete(options?: { timeout?: number, reason?: string }): Promise; @@ -1054,6 +1072,7 @@ declare module 'discord.js' { public readonly members: Collection | null; public roles: Collection; public users: Collection; + public crosspostedChannels: Collection; public toJSON(): object; public static CHANNELS_PATTERN: RegExp; @@ -1886,6 +1905,10 @@ declare module 'discord.js' { | 'LISTENING' | 'WATCHING'; + type MessageFlagsString = 'CROSSPOSTED' + | 'IS_CROSSPOST' + | 'SUPPRESS_EMBEDS'; + interface APIErrror { UNKNOWN_ACCOUNT: number; UNKNOWN_APPLICATION: number; @@ -2128,11 +2151,17 @@ declare module 'discord.js' { name?: string; } - interface GroupActivity { + interface MessageActivity { partyID: string; type: number; } + interface MessageReference { + channelID: string; + guildID: string; + messageID: string | null; + } + type GuildAuditLogsAction = keyof GuildAuditLogsActions; interface GuildAuditLogsActions { @@ -2196,7 +2225,7 @@ declare module 'discord.js' { interface GuildCreateChannelOptions { permissionOverwrites?: OverwriteResolvable[] | Collection; topic?: string; - type?: 'text' | 'voice' | 'category'; + type?: Exclude; nsfw?: boolean; parent?: ChannelResolvable; bitrate?: number; @@ -2373,7 +2402,8 @@ declare module 'discord.js' { | 'USER_PREMIUM_GUILD_SUBSCRIPTION' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2' - | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3'; + | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3' + | 'CHANNEL_FOLLOW_ADD'; interface OverwriteData { allow?: PermissionResolvable; @@ -2611,5 +2641,12 @@ declare module 'discord.js' { type CloseEvent = { wasClean: boolean; code: number; reason: string; target: WebSocket; }; type ErrorEvent = { error: any, message: string, type: string, target: WebSocket; }; + interface CrosspostedChannel { + channelID: Snowflake; + guildID: Snowflake; + type: keyof typeof ChannelType; + name: string; + } + //#endregion } From e936f071c04557e7349ea193de3a17e1dc963562 Mon Sep 17 00:00:00 2001 From: Souji Date: Wed, 2 Oct 2019 14:26:42 +0200 Subject: [PATCH 1195/1359] fix(typings): GuildChannel#parentID is nullable (#3508) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index cb08e2ffe..0e3e95b3c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -794,7 +794,7 @@ declare module 'discord.js' { public readonly members: Collection; public name: string; public readonly parent: CategoryChannel | null; - public parentID: Snowflake; + public parentID: Snowflake | null; public permissionOverwrites: Collection; public readonly permissionsLocked: boolean | null; public readonly position: number; From 345869374845cf4fc4463fd0ac3eea9b582ee1d1 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 4 Oct 2019 10:19:46 +0100 Subject: [PATCH 1196/1359] typings: mark GuildMember#nickname as nullable (#3516) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 0e3e95b3c..1bbb8038f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -851,7 +851,7 @@ declare module 'discord.js' { public joinedTimestamp: number | null; public readonly kickable: boolean; public readonly manageable: boolean; - public nickname: string; + public nickname: string | null; public readonly partial: boolean; public readonly permissions: Readonly; public readonly premiumSince: Date | null; From 48856c08155858ccb5db0f40960846e7f1cee4f9 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 4 Oct 2019 16:44:04 +0200 Subject: [PATCH 1197/1359] fix: set messages deleted when their channel is deleted (#3519) --- src/client/actions/ChannelDelete.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index 9fc0e6d99..fdfc38709 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -2,6 +2,7 @@ const Action = require('./Action'); const { Events } = require('../../util/Constants'); +const DMChannel = require('../../structures/DMChannel'); class ChannelDeleteAction extends Action { constructor(client) { @@ -16,6 +17,11 @@ class ChannelDeleteAction extends Action { if (channel) { client.channels.remove(channel.id); channel.deleted = true; + if (channel.messages && !(channel instanceof DMChannel)) { + for (const message of channel.messages.values()) { + message.deleted = true; + } + } /** * Emitted whenever a channel is deleted. * @event Client#channelDelete From a8f06f251f1af87650711d4c163ed32d909d4e18 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Fri, 4 Oct 2019 15:44:35 +0100 Subject: [PATCH 1198/1359] feat(VoiceState): add VoiceState#streaming (#3521) * feat: add VoiceState#streaming * typings: add VoiceState#streaming --- src/structures/VoiceState.js | 5 +++++ typings/index.d.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index 8e2922a96..c60f6ac65 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -53,6 +53,11 @@ class VoiceState extends Base { * @type {?string} */ this.sessionID = data.session_id; + /** + * Whether this member is streaming using "Go Live" + * @type {boolean} + */ + this.streaming = data.self_stream || false; /** * The ID of the voice channel that this member is in * @type {?Snowflake} diff --git a/typings/index.d.ts b/typings/index.d.ts index 1bbb8038f..456deee71 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1579,6 +1579,7 @@ declare module 'discord.js' { public serverDeaf?: boolean; public serverMute?: boolean; public sessionID?: string; + public streaming: boolean; public readonly speaking: boolean | null; public setDeaf(deaf: boolean, reason?: string): Promise; From a60f8b3d49ffc98061ae4ee51be2092410b81f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Fri, 11 Oct 2019 11:01:16 +0200 Subject: [PATCH 1199/1359] src(constants): add missing APIErrors (#3531) * src: Updated APIErrors * typings: Updated constants --- src/util/Constants.js | 14 ++++++++++++++ typings/index.d.ts | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 7bb3bb50e..da2d2e300 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -508,7 +508,10 @@ exports.VerificationLevels = [ * * MAXIMUM_PINS * * MAXIMUM_ROLES * * MAXIMUM_REACTIONS + * * MAXIMUM_CHANNELS + * * MAXIMUM_INVITES * * UNAUTHORIZED + * * USER_BANNED * * MISSING_ACCESS * * INVALID_ACCOUNT_TYPE * * CANNOT_EXECUTE_ON_DM @@ -528,9 +531,13 @@ exports.VerificationLevels = [ * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL * * INVALID_OR_TAKEN_INVITE_CODE * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE + * * INVALID_OAUTH_TOKEN * * BULK_DELETE_MESSAGE_TOO_OLD + * * INVALID_FORM_BODY * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT + * * INVALID_API_VERSION * * REACTION_BLOCKED + * * RESOURCE_OVERLOADED * @typedef {string} APIError */ exports.APIErrors = { @@ -556,7 +563,10 @@ exports.APIErrors = { MAXIMUM_PINS: 30003, MAXIMUM_ROLES: 30005, MAXIMUM_REACTIONS: 30010, + MAXIMUM_CHANNELS: 30013, + MAXIMUM_INVITES: 30016, UNAUTHORIZED: 40001, + USER_BANNED: 40007, MISSING_ACCESS: 50001, INVALID_ACCOUNT_TYPE: 50002, CANNOT_EXECUTE_ON_DM: 50003, @@ -576,9 +586,13 @@ exports.APIErrors = { CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, INVALID_OR_TAKEN_INVITE_CODE: 50020, CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, + INVALID_OAUTH_TOKEN: 50025, BULK_DELETE_MESSAGE_TOO_OLD: 50034, + INVALID_FORM_BODY: 50035, INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, + INVALID_API_VERSION: 50041, REACTION_BLOCKED: 90001, + RESOURCE_OVERLOADED: 130000, }; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 456deee71..df1b5f2d0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -557,7 +557,10 @@ declare module 'discord.js' { MAXIMUM_PINS: 30003; MAXIMUM_ROLES: 30005; MAXIMUM_REACTIONS: 30010; + MAXIMUM_CHANNELS: 30013; + MAXIMUM_INVITES: 30016; UNAUTHORIZED: 40001; + USER_BANNED: 40007; MISSING_ACCESS: 50001; INVALID_ACCOUNT_TYPE: 50002; CANNOT_EXECUTE_ON_DM: 50003; @@ -576,9 +579,13 @@ declare module 'discord.js' { INVALID_BULK_DELETE_QUANTITY: 50016; CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019; CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021; + INVALID_OAUTH_TOKEN: 50025; BULK_DELETE_MESSAGE_TOO_OLD: 50034; + INVALID_FORM_BODY: 50035; INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036; + INVALID_API_VERSION: 50041; REACTION_BLOCKED: 90001; + RESOURCE_OVERLOADED: 130000; }; VoiceStatus: { CONNECTED: 0; From 79133b4d5e55132b04687c04ba247416aed615e6 Mon Sep 17 00:00:00 2001 From: Tenpi <37512637+Tenpi@users.noreply.github.com> Date: Fri, 11 Oct 2019 18:52:00 -0400 Subject: [PATCH 1200/1359] at(typings): partial Types (#3493) * test * test 2 * update * update * replaced double quotes * Made message.guild and message.member nullable * replaced double quotes again (oops) * missing semicolons * removed fetch from Omit * Added Partialize generic type * Created interfaces (prettier intellisense) * joinedAt/joinedTimestamp are nullable and fixed conflict --- typings/index.d.ts | 86 +++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index df1b5f2d0..bc96ed237 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -170,33 +170,33 @@ declare module 'discord.js' { public sweepMessages(lifetime?: number): number; public toJSON(): object; - public on(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; - public on(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; - public on(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public on(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel | PartialChannel) => void): this; + public on(event: 'channelPinsUpdate', listener: (channel: Channel | PartialChannel, time: Date) => void): this; + public on(event: 'channelUpdate', listener: (oldChannel: Channel | PartialChannel, newChannel: Channel | PartialChannel) => void): this; public on(event: 'debug' | 'warn', listener: (info: string) => void): this; public on(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public on(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public on(event: 'error', listener: (error: Error) => void): this; - public on(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public on(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; - public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; - public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public on(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; + public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; + public on(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; + public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; - public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; - public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; - public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; - public on(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; + public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; + public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; + public on(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public on(event: 'ready', listener: () => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; - public on(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; + public on(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: 'invalidated', listener: () => void): this; @@ -207,33 +207,33 @@ declare module 'discord.js' { public on(event: 'shardResume', listener: (id: number, replayed: number) => void): this; public on(event: string, listener: Function): this; - public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel) => void): this; - public once(event: 'channelPinsUpdate', listener: (channel: Channel, time: Date) => void): this; - public once(event: 'channelUpdate', listener: (oldChannel: Channel, newChannel: Channel) => void): this; + public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel | PartialChannel) => void): this; + public once(event: 'channelPinsUpdate', listener: (channel: Channel | PartialChannel, time: Date) => void): this; + public once(event: 'channelUpdate', listener: (oldChannel: Channel | PartialChannel, newChannel: Channel | PartialChannel) => void): this; public once(event: 'debug' | 'warn', listener: (info: string) => void): this; public once(event: 'disconnect', listener: (event: any, shardID: number) => void): this; public once(event: 'emojiCreate' | 'emojiDelete', listener: (emoji: GuildEmoji) => void): this; public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public once(event: 'error', listener: (error: Error) => void): this; - public once(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User) => void): this; + public once(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; - public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember) => void): this; - public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; - public once(event: 'guildMemberSpeaking', listener: (member: GuildMember, speaking: Readonly) => void): this; - public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember, newMember: GuildMember) => void): this; + public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; + public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; + public once(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; + public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; - public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message) => void): this; - public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; - public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User) => void): this; - public once(event: 'messageUpdate', listener: (oldMessage: Message, newMessage: Message) => void): this; + public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; + public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; + public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; + public once(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; public once(event: 'ready', listener: () => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; - public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel, user: User) => void): this; - public once(event: 'userUpdate', listener: (oldUser: User, newUser: User) => void): this; + public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; + public once(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: 'invalidated', listener: () => void): this; @@ -476,7 +476,7 @@ declare module 'discord.js' { RESUMED: 'resumed'; }; PartialTypes: { - [K in PartialType]: K; + [K in PartialTypes]: K; }; WSEvents: { [K in WSEventType]: K; @@ -645,7 +645,7 @@ declare module 'discord.js' { constructor(client: Client, data?: object); public messages: MessageStore; public recipient: User; - public readonly partial: boolean; + public readonly partial: false; } export class Emoji extends Base { @@ -859,7 +859,7 @@ declare module 'discord.js' { public readonly kickable: boolean; public readonly manageable: boolean; public nickname: string | null; - public readonly partial: boolean; + public readonly partial: false; public readonly permissions: Readonly; public readonly premiumSince: Date | null; public premiumSinceTimestamp: number | null; @@ -945,7 +945,7 @@ declare module 'discord.js' { public activity: MessageActivity | null; public application: ClientApplication | null; public attachments: Collection; - public author: User | null; + public author: User; public channel: TextChannel | DMChannel; public readonly cleanContent: string; public content: string; @@ -963,7 +963,7 @@ declare module 'discord.js' { public readonly member: GuildMember | null; public mentions: MessageMentions; public nonce: string | null; - public readonly partial: boolean; + public readonly partial: false; public readonly pinnable: boolean; public pinned: boolean; public reactions: ReactionStore; @@ -1396,7 +1396,7 @@ declare module 'discord.js' { public readonly dmChannel: DMChannel; public id: Snowflake; public locale: string; - public readonly partial: boolean; + public readonly partial: false; public readonly presence: Presence; public readonly tag: string; public username: string; @@ -2499,6 +2499,19 @@ declare module 'discord.js' { | 'GUILD_MEMBER' | 'MESSAGE'; + type Partialize = { + id: string; + partial: true; + fetch(): Promise; + } & { + [K in keyof Omit]: T[K] | null; + }; + + interface PartialMessage extends Partialize {} + interface PartialChannel extends Partialize {} + interface PartialGuildMember extends Partialize {} + interface PartialUser extends Partialize {} + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; @@ -2605,11 +2618,6 @@ declare module 'discord.js' { compress?: boolean; } - type PartialType = 'USER' - | 'CHANNEL' - | 'GUILD_MEMBER' - | 'MESSAGE'; - type WSEventType = 'READY' | 'RESUMED' | 'GUILD_CREATE' From c3228b426370a6cc5d48cb41e28bbe9f731e8473 Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 11 Oct 2019 17:54:02 -0500 Subject: [PATCH 1201/1359] fix(docslink): partialtypes (#3510) --- docs/topics/partials.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index 2566f3def..f1f05fe28 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -6,7 +6,7 @@ discard the event. With partials, you're able to receive the event, with a Messa ## Opting in -Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](../typedef/PartialType): +Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](/#/docs/main/master/typedef/PartialType): ```js // Accept partial messages and DM channels when emitting events @@ -58,4 +58,4 @@ bot or any bot that relies on still receiving updates to resources you don't hav good example. Currently, the only type of channel that can be uncached is a DM channel, there is no reason why guild channels should -not be cached. \ No newline at end of file +not be cached. From ca1bd61f4f061a9e15f9f636b7c8386417770683 Mon Sep 17 00:00:00 2001 From: Alexander Kashev Date: Fri, 18 Oct 2019 11:30:49 +0200 Subject: [PATCH 1202/1359] typings(Emoji): remove deletable, add deleted, mark nullable props (#3542) --- typings/index.d.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index bc96ed237..52a9a39eb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -651,13 +651,13 @@ declare module 'discord.js' { export class Emoji extends Base { constructor(client: Client, emoji: object); public animated: boolean; - public readonly createdAt: Date; - public readonly createdTimestamp: number; - public readonly deletable: boolean; - public id: Snowflake; + public readonly createdAt: Date | null; + public readonly createdTimestamp: number | null; + public deleted: boolean; + public id: Snowflake | null; public name: string; public readonly identifier: string; - public readonly url: string; + public readonly url: string | null; public toJSON(): object; public toString(): string; } From a61cfc3004e84738e3155dd275800d92cb1a96d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Fri, 18 Oct 2019 11:32:19 +0200 Subject: [PATCH 1203/1359] docs: VoiceStateUpdate always sends an instance of VoiceState (#3537) * docs: VoiceStateUpdate always sends the old * typings: Update definition for voiceStateUpdate event --- src/client/actions/VoiceStateUpdate.js | 2 +- typings/index.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 14e1aa3f8..386bf7783 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -33,7 +33,7 @@ class VoiceStateUpdate extends Action { /** * Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes. * @event Client#voiceStateUpdate - * @param {?VoiceState} oldState The voice state before the update + * @param {VoiceState} oldState The voice state before the update * @param {VoiceState} newState The voice state after the update */ client.emit(Events.VOICE_STATE_UPDATE, oldState, newState); diff --git a/typings/index.d.ts b/typings/index.d.ts index 52a9a39eb..6ff295469 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -197,7 +197,7 @@ declare module 'discord.js' { public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public on(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; - public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; + public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public on(event: 'invalidated', listener: () => void): this; public on(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; @@ -234,7 +234,7 @@ declare module 'discord.js' { public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public once(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; - public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState | undefined, newState: VoiceState) => void): this; + public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; public once(event: 'invalidated', listener: () => void): this; public once(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; From 9e0705cbc39f1be4b03a3cad5e2d216b7d1be4e9 Mon Sep 17 00:00:00 2001 From: rei2hu Date: Fri, 18 Oct 2019 04:55:35 -0500 Subject: [PATCH 1204/1359] fix(Message): check for edited_timestamp in data when patching message (#3535) * check for data.edited_timestamp * actually i should do it like this for consistency * indentation --- src/structures/Message.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index 8a9df07ea..b09f1d348 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -219,7 +219,7 @@ class Message extends Base { const clone = this._clone(); this._edits.unshift(clone); - this.editedTimestamp = new Date(data.edited_timestamp).getTime(); + if ('edited_timestamp' in data) this.editedTimestamp = new Date(data.edited_timestamp).getTime(); if ('content' in data) this.content = data.content; if ('pinned' in data) this.pinned = data.pinned; if ('tts' in data) this.tts = data.tts; From 16db92ede8dc164346eca8482ea008cb21032c7b Mon Sep 17 00:00:00 2001 From: Alexander Kashev Date: Fri, 18 Oct 2019 13:06:34 +0200 Subject: [PATCH 1205/1359] typings(GuildEmoji): restore deletable, remove inherited property deleted (#3543) * Typings: restore deletable on GuildEmoji * Remove inherited property "deleted" --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6ff295469..cfcd96610 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -833,7 +833,7 @@ declare module 'discord.js' { private _roles: string[]; public available: boolean; - public deleted: boolean; + public readonly deletable: boolean; public guild: Guild; public managed: boolean; public requiresColons: boolean; From 9bcb6a04ba43f7d6eebd48c81b84c227cd94a094 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Sun, 27 Oct 2019 11:58:38 -0400 Subject: [PATCH 1206/1359] fix(VoiceConnection): clear timeouts using Client#clearTimeout (#3553) * Update VoiceConnection.js * fix last instance --- src/client/voice/VoiceConnection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b56177c64..57fac3ac8 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -282,7 +282,7 @@ class VoiceConnection extends EventEmitter { * @private */ authenticateFailed(reason) { - clearTimeout(this.connectTimeout); + this.client.clearTimeout(this.connectTimeout); this.emit('debug', `Authenticate failed - ${reason}`); if (this.status === VoiceStatus.AUTHENTICATING) { /** @@ -348,7 +348,7 @@ class VoiceConnection extends EventEmitter { disconnect() { this.emit('closing'); this.emit('debug', 'disconnect() triggered'); - clearTimeout(this.connectTimeout); + this.client.clearTimeout(this.connectTimeout); const conn = this.voiceManager.connections.get(this.channel.guild.id); if (conn === this) this.voiceManager.connections.delete(this.channel.guild.id); this.sendVoiceStateUpdate({ @@ -454,7 +454,7 @@ class VoiceConnection extends EventEmitter { this.status = VoiceStatus.CONNECTED; const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); dispatcher.on('finish', () => { - clearTimeout(this.connectTimeout); + this.client.clearTimeout(this.connectTimeout); this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); /** * Emitted once the connection is ready, when a promise to join a voice channel resolves, From 3a9eb5b92928631edc79e09894ba6bbad90516f7 Mon Sep 17 00:00:00 2001 From: Marwin M Date: Tue, 29 Oct 2019 13:22:21 +0100 Subject: [PATCH 1207/1359] Fix Opus voice streams (#3555) This fixes a wrong assumption about incoming discord voice packets revealed during a recent discord change that broke incoming opus voice streams --- src/client/voice/receiver/PacketHandler.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 7189f1126..8ea9576ef 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -63,7 +63,9 @@ class PacketHandler extends EventEmitter { if (byte === 0) continue; offset += 1 + (0b1111 & (byte >> 4)); } - while (packet[offset] === 0) offset++; + // Skip over undocumented Discord byte + offset++; + packet = packet.slice(offset); } From 3c634b2a262787ab42b0c3c2242d2616874a1c29 Mon Sep 17 00:00:00 2001 From: Jeroen Claassens Date: Mon, 4 Nov 2019 11:25:14 +0100 Subject: [PATCH 1208/1359] chore: mark optional peerDependencies as optional (#3511) --- package.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/package.json b/package.json index e89ca4fc3..cda32552b 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,29 @@ "zlib-sync": "^0.1.4", "zucc": "^0.1.0" }, + "peerDependenciesMeta": { + "@discordjs/uws": { + "optional": true + }, + "bufferutil": { + "optional": true + }, + "erlpack": { + "optional": true + }, + "libsodium-wrappers": { + "optional": true + }, + "sodium": { + "optional": true + }, + "zlib-sync": { + "optional": true + }, + "zucc": { + "optional": true + } + }, "devDependencies": { "@types/node": "^10.12.24", "@types/ws": "^6.0.1", From 2e20e8092bcd14fc992d51ba24e29e11b8ef0dc9 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 4 Nov 2019 11:25:43 +0100 Subject: [PATCH 1209/1359] fix(*Collector): account for a max listener count of 0 (#3504) --- src/structures/MessageCollector.js | 4 ++-- src/structures/ReactionCollector.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 44156cc03..5d45be089 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -39,7 +39,7 @@ class MessageCollector extends Collector { for (const message of messages.values()) this.handleDispose(message); }).bind(this); - this.client.setMaxListeners(this.client.getMaxListeners() + 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_CREATE, this.handleCollect); this.client.on(Events.MESSAGE_DELETE, this.handleDispose); this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); @@ -48,7 +48,7 @@ class MessageCollector extends Collector { this.client.removeListener(Events.MESSAGE_CREATE, this.handleCollect); this.client.removeListener(Events.MESSAGE_DELETE, this.handleDispose); this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); - this.client.setMaxListeners(this.client.getMaxListeners() - 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); } diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index b4826100f..8e84fb672 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -44,7 +44,7 @@ class ReactionCollector extends Collector { this.empty = this.empty.bind(this); - this.client.setMaxListeners(this.client.getMaxListeners() + 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.on(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.on(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); @@ -53,7 +53,7 @@ class ReactionCollector extends Collector { this.client.removeListener(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); - this.client.setMaxListeners(this.client.getMaxListeners() - 1); + if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); this.on('collect', (reaction, user) => { From e26697f07d00955e14d6756ef8a1230cc07f1b81 Mon Sep 17 00:00:00 2001 From: Albus Dumbledore Date: Mon, 4 Nov 2019 15:59:19 +0530 Subject: [PATCH 1210/1359] docs(readme): table of contents (#3539) --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 12ee3a2bd..71efbfdd8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,17 @@

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

From 1352bff2fd969092b48368ebc1f05a080572f827 Mon Sep 17 00:00:00 2001 From: Edward Wang <37031713+ewang20027@users.noreply.github.com> Date: Sat, 9 Nov 2019 11:52:55 -0800 Subject: [PATCH 1215/1359] docs(README): link to guide page instead of source (#3566) * Fixed the Update Guide Link Original link pointed to https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md, which is invalid. I'm not sure if the link I put (https://github.com/discordjs/guide/blob/master/guide/additional-info/changes-in-v12.md) is the correct one, but I will assume it is. * Used link to the DJS guide. --- docs/general/welcome.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index da1e02e89..d13e623b0 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -77,7 +77,7 @@ client.login('token'); * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) * [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) * [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable - See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. + See also the WIP [Update Guide](https://discordjs.guide/additional-info/changes-in-v12.html) also including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) From 1bcc0c2e1d7a8824529e63bee304ad1cb54ddfe3 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Tue, 19 Nov 2019 13:51:26 -0700 Subject: [PATCH 1216/1359] feat(GuildAuditLogs): add new event types (#3584) * adds more audit-log entries this adds additional audit-log types from https://discordapp/discord-api-docs/pull/1191 * typings for new audit-log entries * typings for new audit-log entries * fix action numbers --- src/structures/GuildAuditLogs.js | 18 ++++++++++++++++++ typings/index.d.ts | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 924219194..53494fa45 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -53,6 +53,9 @@ const Targets = { * * MEMBER_BAN_REMOVE: 23 * * MEMBER_UPDATE: 24 * * MEMBER_ROLE_UPDATE: 25 + * * MEMBER_MOVE: 26 + * * MEMBER_DISCONNECT: 27 + * * BOT_ADD: 28, * * ROLE_CREATE: 30 * * ROLE_UPDATE: 31 * * ROLE_DELETE: 32 @@ -66,6 +69,12 @@ const Targets = { * * EMOJI_UPDATE: 61 * * EMOJI_DELETE: 62 * * MESSAGE_DELETE: 72 + * * MESSAGE_BULK_DELETE: 73 + * * MESSAGE_PIN: 74 + * * MESSAGE_UNPIN: 75 + * * INTEGRATION_CREATE: 80 + * * INTEGRATION_UPDATE: 81 + * * INTEGRATION_DELETE: 82 * @typedef {?number|string} AuditLogAction */ @@ -89,6 +98,9 @@ const Actions = { MEMBER_BAN_REMOVE: 23, MEMBER_UPDATE: 24, MEMBER_ROLE_UPDATE: 25, + MEMBER_MOVE: 26, + MEMBER_DISCONNECT: 27, + BOT_ADD: 28, ROLE_CREATE: 30, ROLE_UPDATE: 31, ROLE_DELETE: 32, @@ -102,6 +114,12 @@ const Actions = { EMOJI_UPDATE: 61, EMOJI_DELETE: 62, MESSAGE_DELETE: 72, + MESSAGE_BULK_DELETE: 73, + MESSAGE_PIN: 74, + MESSAGE_UNPIN: 75, + INTEGRATION_CREATE: 80, + INTEGRATION_UPDATE: 81, + INTEGRATION_DELETE: 82, }; diff --git a/typings/index.d.ts b/typings/index.d.ts index da78cc7e2..1da0d9d2d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2188,6 +2188,9 @@ declare module 'discord.js' { MEMBER_BAN_REMOVE?: number; MEMBER_UPDATE?: number; MEMBER_ROLE_UPDATE?: number; + MEMBER_MOVE?: number; + MEMBER_DISCONNECT?: number; + BOT_ADD?: number; ROLE_CREATE?: number; ROLE_UPDATE?: number; ROLE_DELETE?: number; @@ -2201,6 +2204,12 @@ declare module 'discord.js' { EMOJI_UPDATE?: number; EMOJI_DELETE?: number; MESSAGE_DELETE?: number; + MESSAGE_BULK_DELETE?: number; + MESSAGE_PIN?: number; + MESSAGE_UNPIN?: number; + INTEGRATION_CREATE?: number; + INTEGRATION_UPDATE?: number; + INTEGRATION_DELETE?: number; } type GuildAuditLogsActionType = 'CREATE' From d39f17925d1bc9a5f25adbc2f52f7807c4e8b163 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Tue, 19 Nov 2019 15:54:35 -0500 Subject: [PATCH 1217/1359] fix(Invite): fix valueOf returning undefined (#3582) * Update Invite.js * Fix typings * Fix ESLint errors --- src/structures/Invite.js | 4 ++++ typings/index.d.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/src/structures/Invite.js b/src/structures/Invite.js index d5e9f895d..4d048b3f1 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -183,6 +183,10 @@ class Invite extends Base { guild: 'guildID', }); } + + valueOf() { + return this.code; + } } module.exports = Invite; diff --git a/typings/index.d.ts b/typings/index.d.ts index 1da0d9d2d..3cae781a9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -878,6 +878,7 @@ declare module 'discord.js' { public setNickname(nickname: string, reason?: string): Promise; public toJSON(): object; public toString(): string; + public valueOf(): string; } export class Integration extends Base { From cbde819b6a21e008625c20ca575d44f4e450bd2f Mon Sep 17 00:00:00 2001 From: Jyguy Date: Tue, 19 Nov 2019 18:21:47 -0500 Subject: [PATCH 1218/1359] typings(GuildAuditLogsFetchOptions): specify concrete type of 'type' property (#3586) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 3cae781a9..20851d7a2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2222,7 +2222,7 @@ declare module 'discord.js' { before?: Snowflake | GuildAuditLogsEntry; limit?: number; user?: UserResolvable; - type?: string | number; + type?: GuildAuditLogsAction | number; } type GuildAuditLogsTarget = keyof GuildAuditLogsTargets; From 100360705ae29216f14129bc7a37f3870ec58fc6 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Wed, 20 Nov 2019 12:11:23 -0500 Subject: [PATCH 1219/1359] fix(APIRouter): use proper symbol for util.inspect (#3589) --- src/rest/APIRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rest/APIRouter.js b/src/rest/APIRouter.js index 4d1fb5d6b..efb71fa35 100644 --- a/src/rest/APIRouter.js +++ b/src/rest/APIRouter.js @@ -4,7 +4,7 @@ const noop = () => {}; // eslint-disable-line no-empty-function const methods = ['get', 'post', 'delete', 'patch', 'put']; const reflectors = [ 'toString', 'valueOf', 'inspect', 'constructor', - Symbol.toPrimitive, Symbol.for('util.inspect.custom'), + Symbol.toPrimitive, Symbol.for('nodejs.util.inspect.custom'), ]; function buildRoute(manager) { From 1b1289b35e2f8cd21341351549e6ac52fd08cb34 Mon Sep 17 00:00:00 2001 From: BannerBomb Date: Mon, 25 Nov 2019 10:17:30 -0500 Subject: [PATCH 1220/1359] misc(index): export Store- and NewsChannel (#3594) * Added Store and NewsChannel to exports Added the StoreChannel and NewsChannel structures to the module exports. * keeping the list in alphabetical order I moved the StoreChannel and NewsChannel exports that I added in the last commit in their right position to keep things alphabetized. --- src/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.js b/src/index.js index 77c56336d..2d776e7ca 100644 --- a/src/index.js +++ b/src/index.js @@ -79,6 +79,7 @@ module.exports = { MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), MessageReaction: require('./structures/MessageReaction'), + NewsChannel: require('./structures/NewsChannel'), PermissionOverwrites: require('./structures/PermissionOverwrites'), Presence: require('./structures/Presence').Presence, ClientPresence: require('./structures/ClientPresence'), @@ -86,6 +87,7 @@ module.exports = { ReactionEmoji: require('./structures/ReactionEmoji'), RichPresenceAssets: require('./structures/Presence').RichPresenceAssets, Role: require('./structures/Role'), + StoreChannel: require('./structures/StoreChannel'), Team: require('./structures/Team'), TeamMember: require('./structures/TeamMember'), TextChannel: require('./structures/TextChannel'), From 2ca74d6b63392c09a388c58f9b04945e884646c6 Mon Sep 17 00:00:00 2001 From: Jeroen Claassens Date: Thu, 5 Dec 2019 13:13:42 +0100 Subject: [PATCH 1221/1359] feat(Activity): support for CUSTOM_STATUS activity type (#3353) * feat: support for custom status in activity * nit(typings): order properties --- src/structures/Presence.js | 22 ++++++++++++++++++++++ src/util/Constants.js | 2 ++ typings/index.d.ts | 6 +++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 8c7d22b0d..2eccf7cb2 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -3,6 +3,7 @@ const Util = require('../util/Util'); const ActivityFlags = require('../util/ActivityFlags'); const { ActivityTypes } = require('../util/Constants'); +const Emoji = require('./Emoji'); /** * Activity sent in a message. @@ -205,6 +206,18 @@ class Activity { * @type {Readonly} */ this.flags = new ActivityFlags(data.flags).freeze(); + + /** + * Emoji for a custom activity + * @type {?Emoji} + */ + this.emoji = data.emoji ? new Emoji(presence.client, data.emoji) : null; + + /** + * Creation date of the activity + * @type {number} + */ + this.createdTimestamp = new Date(data.created_at).getTime(); } /** @@ -221,6 +234,15 @@ class Activity { ); } + /** + * The time the activity was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + /** * When concatenated with a string, this automatically returns the activities' name instead of the Activity object. * @returns {string} diff --git a/src/util/Constants.js b/src/util/Constants.js index da2d2e300..12ddfbf8f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -411,6 +411,7 @@ exports.MessageTypes = [ * * STREAMING * * LISTENING * * WATCHING + * * CUSTOM_STATUS * @typedef {string} ActivityType */ exports.ActivityTypes = [ @@ -418,6 +419,7 @@ exports.ActivityTypes = [ 'STREAMING', 'LISTENING', 'WATCHING', + 'CUSTOM_STATUS', ]; exports.ChannelTypes = { diff --git a/typings/index.d.ts b/typings/index.d.ts index 20851d7a2..abab6b954 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -25,7 +25,10 @@ declare module 'discord.js' { constructor(presence: Presence, data?: object); public applicationID: Snowflake | null; public assets: RichPresenceAssets | null; + public readonly createdAt: Date; + public createdTimestamp: number; public details: string | null; + public emoji: Emoji | null; public name: string; public party: { id: string | null; @@ -1913,7 +1916,8 @@ declare module 'discord.js' { type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' - | 'WATCHING'; + | 'WATCHING' + | 'CUSTOM_STATUS'; type MessageFlagsString = 'CROSSPOSTED' | 'IS_CROSSPOST' From bb8333a4f9d3171f41a72e3bd5d5c6118fe22025 Mon Sep 17 00:00:00 2001 From: Clemens E <43886026+Clemens-E@users.noreply.github.com> Date: Fri, 6 Dec 2019 12:56:29 +0100 Subject: [PATCH 1222/1359] Handle voice errors outside of authenticated event (#3520) --- src/client/voice/ClientVoiceManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index 784136934..cf7526623 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -93,12 +93,13 @@ class ClientVoiceManager { reject(reason); }); + connection.on('error', reject); + connection.once('authenticated', () => { connection.once('ready', () => { resolve(connection); connection.removeListener('error', reject); }); - connection.on('error', reject); connection.once('disconnect', () => this.connections.delete(channel.guild.id)); }); }); From 4585d965b430ed0661ac8d3e32b738f27b4c9d42 Mon Sep 17 00:00:00 2001 From: sillyfrog <816454+sillyfrog@users.noreply.github.com> Date: Fri, 6 Dec 2019 21:59:57 +1000 Subject: [PATCH 1223/1359] Start/Stop speaking events on UDP packets (#3578) * Start/Stop speaking using incomming UDP packets * Fix ESLint errors * Updates for styling consistency Co-Authored-By: Gryffon Bellish * Minor improvements * Acutally use previousTimeout * Use BaseClient setTimeout and refresh() * Update README to match node version for refresh() * Update comment to match startSpeaking * Correctly report Priority bit * Fix ESlint errors --- README.md | 2 +- src/client/voice/VoiceConnection.js | 11 ++++--- src/client/voice/networking/VoiceWebSocket.js | 4 +-- src/client/voice/receiver/PacketHandler.js | 31 ++++++++++++++++--- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6c12826c5..d667ab1ff 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 10.0.0 or newer is required.** +**Node.js 10.2.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 57fac3ac8..540dd8c74 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -422,7 +422,7 @@ class VoiceConnection extends EventEmitter { udp.on('error', err => this.emit('error', err)); ws.on('ready', this.onReady.bind(this)); ws.on('sessionDescription', this.onSessionDescription.bind(this)); - ws.on('speaking', this.onSpeaking.bind(this)); + ws.on('startSpeaking', this.onStartSpeaking.bind(this)); this.sockets.ws.connect(); } @@ -465,16 +465,19 @@ class VoiceConnection extends EventEmitter { }); } + onStartSpeaking({ user_id, ssrc, speaking }) { + this.ssrcMap.set(+ssrc, { userID: user_id, speaking: speaking }); + } + /** * Invoked when a speaking event is received. * @param {Object} data The received data * @private */ - onSpeaking({ user_id, ssrc, speaking }) { + onSpeaking({ user_id, speaking }) { speaking = new Speaking(speaking).freeze(); const guild = this.channel.guild; const user = this.client.users.get(user_id); - this.ssrcMap.set(+ssrc, user_id); const old = this._speaking.get(user_id); this._speaking.set(user_id, speaking); /** @@ -504,7 +507,7 @@ class VoiceConnection extends EventEmitter { } } - play() {} // eslint-disable-line no-empty-function + play() { } // eslint-disable-line no-empty-function } PlayInterface.applyToClass(VoiceConnection); diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index 6ddabe626..ac6405e14 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -201,9 +201,9 @@ class VoiceWebSocket extends EventEmitter { /** * Emitted whenever a speaking packet is received. * @param {Object} data - * @event VoiceWebSocket#speaking + * @event VoiceWebSocket#startSpeaking */ - this.emit('speaking', packet.d); + this.emit('startSpeaking', packet.d); break; default: /** diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index 8ea9576ef..d468ababc 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -3,6 +3,10 @@ const secretbox = require('../util/Secretbox'); const EventEmitter = require('events'); +// The delay between packets when a user is considered to have stopped speaking +// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200 +const DISCORD_SPEAKING_DELAY = 250; + class Readable extends require('stream').Readable { _read() {} } // eslint-disable-line no-empty-function class PacketHandler extends EventEmitter { @@ -11,6 +15,7 @@ class PacketHandler extends EventEmitter { this.nonce = Buffer.alloc(24); this.receiver = receiver; this.streams = new Map(); + this.speakingTimeouts = new Map(); } get connection() { @@ -72,13 +77,29 @@ class PacketHandler extends EventEmitter { return packet; } - userFromSSRC(ssrc) { return this.connection.client.users.get(this.connection.ssrcMap.get(ssrc)); } - push(buffer) { const ssrc = buffer.readUInt32BE(8); - const user = this.userFromSSRC(ssrc); - if (!user) return; - let stream = this.streams.get(user.id); + const userStat = this.connection.ssrcMap.get(ssrc); + if (!userStat) return; + + let speakingTimeout = this.speakingTimeouts.get(ssrc); + if (typeof speakingTimeout === 'undefined') { + this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: userStat.speaking }); + speakingTimeout = this.receiver.connection.client.setTimeout(() => { + try { + this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: 0 }); + this.receiver.connection.client.clearTimeout(speakingTimeout); + this.speakingTimeouts.delete(ssrc); + } catch (ex) { + // Connection already closed, ignore + } + }, DISCORD_SPEAKING_DELAY); + this.speakingTimeouts.set(ssrc, speakingTimeout); + } else { + speakingTimeout.refresh(); + } + + let stream = this.streams.get(userStat.userID); if (!stream) return; stream = stream.stream; const opusPacket = this.parseBuffer(buffer); From 123713305ad5a6aa1e5205a53713494009740aef Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+duckthecuck@users.noreply.github.com> Date: Sun, 8 Dec 2019 13:52:03 -0500 Subject: [PATCH 1224/1359] docs(ReactionStore): resolveID takes a reaction, not role (#3617) * Docs: ReactionStore.resolveID should take Reaction, not role * Make param lowercase Co-Authored-By: SpaceEEC --- src/stores/ReactionStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index b3910ba4c..5092e861f 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -38,7 +38,7 @@ class ReactionStore extends DataStore { * @method resolveID * @memberof ReactionStore * @instance - * @param {MessageReactionResolvable} role The role resolvable to resolve + * @param {MessageReactionResolvable} reaction The MessageReaction to resolve * @returns {?Snowflake} */ From 330d5db5868ad61bbdae5ba0b2090d10d76ae9c9 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Sun, 15 Dec 2019 12:20:15 -0700 Subject: [PATCH 1225/1359] feat(Webhook): addition of Webhook#avatarURL function (#3625) * feat: addition of Webhook#avatarURL * typings: added Webhook#avatarURL * fix: trailing space * docs: fixed jsdoc function description * fix: typo --- src/structures/Webhook.js | 10 ++++++++++ typings/index.d.ts | 1 + 2 files changed, 11 insertions(+) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 2ed6137b4..da7dcd12f 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -220,6 +220,16 @@ class Webhook { return this.client.options.http.api + this.client.api.webhooks(this.id, this.token); } + /** + * A link to the webhook's avatar. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + avatarURL({ format, size } = {}) { + if (!this.avatar) return null; + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size); + } + static applyToClass(structure) { for (const prop of [ 'send', diff --git a/typings/index.d.ts b/typings/index.d.ts index abab6b954..cb2ccda02 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1620,6 +1620,7 @@ declare module 'discord.js' { export class Webhook extends WebhookMixin() { constructor(client: Client, data?: object); public avatar: string; + public avatarURL(options?: AvatarOptions): string | null; public channelID: Snowflake; public guildID: Snowflake; public name: string; From 43782839ec51519deb27977573fb44e516c35ec2 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 15 Dec 2019 19:23:06 +0000 Subject: [PATCH 1226/1359] feat: add new MessageFlags.FLAGS & User#system (#3603) * feat: add new FLAGS * feat: add system property * typings: add User#system & new MessageFlagsStrings --- src/structures/User.js | 7 +++++++ src/util/MessageFlags.js | 4 ++++ typings/index.d.ts | 5 ++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/structures/User.js b/src/structures/User.js index c2276c4f2..b5358be7c 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -59,6 +59,13 @@ class User extends Base { if (typeof data.bot !== 'undefined') this.bot = Boolean(data.bot); + /** + * Whether the user is an Official Discord System user (part of the urgent message system) + * @type {?boolean} + * @name User#system + */ + if (typeof data.system !== 'undefined') this.system = Boolean(data.system); + /** * The locale of the user's client (ISO 639-1) * @type {?string} diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js index b1c86b7d6..4863e6710 100644 --- a/src/util/MessageFlags.js +++ b/src/util/MessageFlags.js @@ -13,6 +13,8 @@ class MessageFlags extends BitField {} * * `CROSSPOSTED` * * `IS_CROSSPOST` * * `SUPPRESS_EMBEDS` + * * `SOURCE_MESSAGE_DELETED` + * * `URGENT` * @type {Object} * @see {@link https://discordapp.com/developers/docs/resources/channel#message-object-message-flags} */ @@ -20,6 +22,8 @@ MessageFlags.FLAGS = { CROSSPOSTED: 1 << 0, IS_CROSSPOST: 1 << 1, SUPPRESS_EMBEDS: 1 << 2, + SOURCE_MESSAGE_DELETED: 1 << 3, + URGENT: 1 << 4, }; module.exports = MessageFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index cb2ccda02..b0774df23 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1403,6 +1403,7 @@ declare module 'discord.js' { public locale: string; public readonly partial: false; public readonly presence: Presence; + public system?: boolean; public readonly tag: string; public username: string; public avatarURL(options?: AvatarOptions): string | null; @@ -1922,7 +1923,9 @@ declare module 'discord.js' { type MessageFlagsString = 'CROSSPOSTED' | 'IS_CROSSPOST' - | 'SUPPRESS_EMBEDS'; + | 'SUPPRESS_EMBEDS' + | 'SOURCE_MESSAGE_DELETED' + | 'URGENT'; interface APIErrror { UNKNOWN_ACCOUNT: number; From f56b442e83e4016015c9123d4308dbf9209eea76 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Sun, 15 Dec 2019 14:26:09 -0500 Subject: [PATCH 1227/1359] typings(Bitfield): use IterableIterator instead of Iterator (#3599) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index b0774df23..9eb0b4532 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -124,7 +124,7 @@ declare module 'discord.js' { public toArray(): S[]; public toJSON(): number; public valueOf(): number; - public [Symbol.iterator](): Iterator; + public [Symbol.iterator](): IterableIterator; public static FLAGS: object; public static resolve(bit?: BitFieldResolvable): number; } From 5519d6fbaa61abccc27b742f97ad725bf4259265 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 15 Dec 2019 21:45:27 +0200 Subject: [PATCH 1228/1359] src: sharding cleanup and checkReady rewrite (#3393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * src: Step 1 of who knows how many * src: Remove accidentally committed test file * src: Remove useless added property in package.json * docs: Trailing spaces, come back >.> * src: Buhbye uws, we will miss you..not! * src: Move 'auto' shard selection from totalShardCount to shards * src: tweak * src: Filter out floats from shard IDs You want half of a shard or what? * src: Misc cleanup and bugfix for GUILD_BAN_ADD * src: Rewrite checkReady * src: Misse this while merging master into my branch * typings: Bring these up to date * typings: Forgot allReady event * src: Don't checkReady if the shard isn't waiting for guilds * src: Fix a possible bug for when the ws dies and the session becomes -1 * src: Hopefully fix last edge case that could case a shard to infinitely boot loop * src: Rename totalShardCount to shardCount * src: Small bugfix * src: Correct error message for shardCount being imvalid Co-Authored-By: bdistin * src: Small tweaks * src: If this doesn't fix the issues I'm gonna throw a brick at my PC * src: I swear, STOP BREAKING * src: *groans at a certain snake* * src: Use undefined instead of null on destroy in close event Setting it to null sets the close code to null, which causes a WebSocket error to be thrown. The error is thrown from WebSocket, although there is no connection alive. Fun times! * src: @SpaceEEC's requested changes * src: Remove zucc from discord.js Discord is removing support for it, sooo... Bye bye * src: Missed this * src: Apply @kyranet's suggestions Co-Authored-By: Antonio Román * src: @kyranet's suggestions * src: Remove pako, update debug messages - Pako is officially gone from both enviroments Install zlib-sync on node.js if you want it - Improve a few debug messages some more - Discover that internal sharding works in browsers but please don't do that --- .eslintrc.json | 2 +- README.md | 5 +- docs/general/welcome.md | 11 +- package.json | 11 +- src/WebSocket.js | 13 +- src/client/Client.js | 37 +-- src/client/websocket/WebSocketManager.js | 104 ++++---- src/client/websocket/WebSocketShard.js | 244 +++++++++++------- .../websocket/handlers/GUILD_BAN_ADD.js | 2 +- src/client/websocket/handlers/GUILD_CREATE.js | 16 +- src/client/websocket/handlers/READY.js | 2 - src/sharding/Shard.js | 2 +- src/sharding/ShardClientUtil.js | 2 +- src/util/Constants.js | 15 +- typings/index.d.ts | 22 +- 15 files changed, 280 insertions(+), 208 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index a4f08d689..549500347 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": "eslint:recommended", "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2019 }, "env": { "es6": true, diff --git a/README.md b/README.md index d667ab1ff..2003f60d5 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,12 @@ For production bots, using node-opus should be considered a necessity, especiall ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) -- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) -- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`) -- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`) +- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`) +- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`) ## Example usage ```js diff --git a/docs/general/welcome.md b/docs/general/welcome.md index d13e623b0..52cb3135f 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -24,8 +24,8 @@ v12 is still very much a work-in-progress, as we're aiming to make it the best i Only use it if you are fond of living life on the bleeding edge. ## About -discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to interact with the -[Discord API](https://discordapp.com/developers/docs/intro) very easily. +discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to easily interact with the +[Discord API](https://discordapp.com/developers/docs/intro). - Object-oriented - Predictable abstractions @@ -46,14 +46,13 @@ Using opusscript is only recommended for development environments where node-opu For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for significantly faster WebSocket data inflation (`npm install zlib-sync`) -- [zucc](https://www.npmjs.com/package/zucc) for significantly faster WebSocket data inflation (`npm install zucc`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) - [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`) -- [uws](https://www.npmjs.com/package/@discordjs/uws) for a much faster WebSocket connection (`npm install @discordjs/uws`) -- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection when *not* using uws (`npm install bufferutil`) +- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`) +- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`) ## Example usage ```js diff --git a/package.json b/package.json index cda32552b..c8decaa3b 100644 --- a/package.json +++ b/package.json @@ -39,20 +39,18 @@ "abort-controller": "^3.0.0", "form-data": "^2.3.3", "node-fetch": "^2.3.0", - "pako": "^1.0.8", "prism-media": "^1.0.0", "setimmediate": "^1.0.5", "tweetnacl": "^1.0.1", - "ws": "^6.1.3" + "ws": "^7.2.0" }, "peerDependencies": { - "@discordjs/uws": "^11.149.1", "bufferutil": "^4.0.1", "erlpack": "discordapp/erlpack", "libsodium-wrappers": "^0.7.4", "sodium": "^3.0.2", - "zlib-sync": "^0.1.4", - "zucc": "^0.1.0" + "utf-8-validate": "^5.0.2", + "zlib-sync": "^0.1.6" }, "peerDependenciesMeta": { "@discordjs/uws": { @@ -97,8 +95,6 @@ "browser": { "https": false, "ws": false, - "uws": false, - "@discordjs/uws": false, "erlpack": false, "prism-media": false, "opusscript": false, @@ -107,7 +103,6 @@ "sodium": false, "worker_threads": false, "zlib-sync": false, - "zucc": false, "src/sharding/Shard.js": false, "src/sharding/ShardClientUtil.js": false, "src/sharding/ShardingManager.js": false, diff --git a/src/WebSocket.js b/src/WebSocket.js index 8c61dfb49..90dd51bb5 100644 --- a/src/WebSocket.js +++ b/src/WebSocket.js @@ -1,10 +1,13 @@ 'use strict'; const { browser } = require('./util/Constants'); + +let erlpack; + try { - var erlpack = require('erlpack'); + erlpack = require('erlpack'); if (!erlpack.pack) erlpack = null; -} catch (err) {} // eslint-disable-line no-empty +} catch {} // eslint-disable-line no-empty let TextDecoder; @@ -13,11 +16,7 @@ if (browser) { exports.WebSocket = window.WebSocket; // eslint-disable-line no-undef } else { TextDecoder = require('util').TextDecoder; - try { - exports.WebSocket = require('@discordjs/uws'); - } catch (err) { - exports.WebSocket = require('ws'); - } + exports.WebSocket = require('ws'); } const ab = new TextDecoder(); diff --git a/src/client/Client.js b/src/client/Client.js index 3be8e3115..a1ab6672e 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -36,7 +36,7 @@ class Client extends BaseClient { try { // Test if worker threads module is present and used data = require('worker_threads').workerData || data; - } catch (_) { + } catch { // Do nothing } @@ -46,25 +46,25 @@ class Client extends BaseClient { } } - if (this.options.totalShardCount === DefaultOptions.totalShardCount) { - if ('TOTAL_SHARD_COUNT' in data) { - this.options.totalShardCount = Number(data.TOTAL_SHARD_COUNT); + if (this.options.shardCount === DefaultOptions.shardCount) { + if ('SHARD_COUNT' in data) { + this.options.shardCount = Number(data.SHARD_COUNT); } else if (Array.isArray(this.options.shards)) { - this.options.totalShardCount = this.options.shards.length; - } else { - this.options.totalShardCount = this.options.shardCount; + this.options.shardCount = this.options.shards.length; } } - if (typeof this.options.shards === 'undefined' && typeof this.options.shardCount === 'number') { + const typeofShards = typeof this.options.shards; + + if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); } - if (typeof this.options.shards === 'number') this.options.shards = [this.options.shards]; + if (typeofShards === 'number') this.options.shards = [this.options.shards]; - if (typeof this.options.shards !== 'undefined') { + if (Array.isArray(this.options.shards)) { this.options.shards = [...new Set( - this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity) + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)) )]; } @@ -198,7 +198,9 @@ class Client extends BaseClient { async login(token = this.token) { if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); - this.emit(Events.DEBUG, `Provided token: ${token}`); + this.emit(Events.DEBUG, + `Provided token: ${token.split('.').map((val, i) => i > 1 ? val.replace(/./g, '*') : val).join('.')}` + ); if (this.options.presence) { this.options.ws.presence = await this.presence._parse(this.options.presence); @@ -363,14 +365,15 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { // eslint-disable-line complexity - if (options.shardCount !== 'auto' && (typeof options.shardCount !== 'number' || isNaN(options.shardCount))) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number or "auto"'); + if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shardCount', 'a number greater than or equal to 1'); } - if (options.shards && !Array.isArray(options.shards)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'shards', 'a number or array'); + if (options.shards && + !(options.shards === 'auto' || Array.isArray(options.shards)) + ) { + throw new TypeError('CLIENT_INVALID_OPTION', 'shards', '\'auto\', a number or array of numbers'); } if (options.shards && !options.shards.length) throw new RangeError('CLIENT_INVALID_PROVIDED_SHARDS'); - if (options.shardCount < 1) throw new RangeError('CLIENT_INVALID_OPTION', 'shardCount', 'at least 1'); if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('CLIENT_INVALID_OPTION', 'messageCacheMaxSize', 'a number'); } diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 8668d2de5..71cbf93c0 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -19,6 +19,7 @@ const BeforeReadyWhitelist = [ ]; const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; +const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; /** * The WebSocket manager for this client. @@ -47,9 +48,9 @@ class WebSocketManager extends EventEmitter { /** * The amount of shards this manager handles * @private - * @type {number|string} + * @type {number} */ - this.totalShards = this.client.options.shardCount; + this.totalShards = this.client.options.shards.length; /** * A collection of all shards this manager handles @@ -143,25 +144,25 @@ class WebSocketManager extends EventEmitter { const { total, remaining, reset_after } = sessionStartLimit; this.debug(`Fetched Gateway Information - URL: ${gatewayURL} - Recommended Shards: ${recommendedShards}`); + URL: ${gatewayURL} + Recommended Shards: ${recommendedShards}`); this.debug(`Session Limit Information - Total: ${total} - Remaining: ${remaining}`); + Total: ${total} + Remaining: ${remaining}`); this.gateway = `${gatewayURL}/`; - if (this.totalShards === 'auto') { + const { shards } = this.client.options; + + if (shards === 'auto') { this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); - this.totalShards = this.client.options.shardCount = this.client.options.totalShardCount = recommendedShards; - if (typeof this.client.options.shards === 'undefined' || !this.client.options.shards.length) { + this.totalShards = this.client.options.shardCount = recommendedShards; + if (shards === 'auto' || !this.client.options.shards.length) { this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); } } - const { shards } = this.client.options; - if (Array.isArray(shards)) { this.totalShards = shards.length; this.debug(`Spawning shards: ${shards.join(', ')}`); @@ -190,15 +191,17 @@ class WebSocketManager extends EventEmitter { this.shardQueue.delete(shard); if (!shard.eventsAttached) { - shard.on(ShardEvents.READY, () => { + shard.on(ShardEvents.ALL_READY, unavailableGuilds => { /** * Emitted when a shard turns ready. * @event Client#shardReady * @param {number} id The shard ID that turned ready + * @param {?Set} unavailableGuilds Set of unavailable guild IDs, if any */ - this.client.emit(Events.SHARD_READY, shard.id); + this.client.emit(Events.SHARD_READY, shard.id, unavailableGuilds); if (!this.shardQueue.size) this.reconnecting = false; + this.checkShardsReady(); }); shard.on(ShardEvents.CLOSE, event => { @@ -214,8 +217,8 @@ class WebSocketManager extends EventEmitter { return; } - if (event.code === 1000 || event.code === 4006) { - // Any event code in this range cannot be resumed. + if (UNRESUMABLE_CLOSE_CODES.includes(event.code)) { + // These event codes cannot be resumed shard.sessionID = undefined; } @@ -226,27 +229,23 @@ class WebSocketManager extends EventEmitter { */ this.client.emit(Events.SHARD_RECONNECTING, shard.id); + this.shardQueue.add(shard); + if (shard.sessionID) { this.debug(`Session ID is present, attempting an immediate reconnect...`, shard); - shard.connect().catch(() => null); - return; + this.reconnect(true); + } else { + shard.destroy(undefined, true); + this.reconnect(); } - - shard.destroy(); - - this.shardQueue.add(shard); - this.reconnect(); }); shard.on(ShardEvents.INVALID_SESSION, () => { this.client.emit(Events.SHARD_RECONNECTING, shard.id); - - this.shardQueue.add(shard); - this.reconnect(); }); shard.on(ShardEvents.DESTROYED, () => { - this.debug('Shard was destroyed but no WebSocket connection existed... Reconnecting...', shard); + this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); this.client.emit(Events.SHARD_RECONNECTING, shard.id); @@ -264,7 +263,7 @@ class WebSocketManager extends EventEmitter { } catch (error) { if (error && error.code && UNRECOVERABLE_CLOSE_CODES.includes(error.code)) { throw new DJSError(WSCodes[error.code]); - // Undefined if session is invalid, error event (or uws' event mimicking it) for regular closes + // Undefined if session is invalid, error event for regular closes } else if (!error || error.code) { this.debug('Failed to connect to the gateway, requeueing...', shard); this.shardQueue.add(shard); @@ -285,14 +284,15 @@ class WebSocketManager extends EventEmitter { /** * Handles reconnects for this manager. + * @param {boolean} [skipLimit=false] IF this reconnect should skip checking the session limit * @private * @returns {Promise} */ - async reconnect() { + async reconnect(skipLimit = false) { if (this.reconnecting || this.status !== Status.READY) return false; this.reconnecting = true; try { - await this._handleSessionLimit(); + if (!skipLimit) await this._handleSessionLimit(); await this.createShards(); } catch (error) { this.debug(`Couldn't reconnect or fetch information about the gateway. ${error}`); @@ -340,7 +340,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); this.destroyed = true; this.shardQueue.clear(); - for (const shard of this.shards.values()) shard.destroy(); + for (const shard of this.shards.values()) shard.destroy(1000, true); } /** @@ -356,8 +356,8 @@ class WebSocketManager extends EventEmitter { remaining = session_start_limit.remaining; resetAfter = session_start_limit.reset_after; this.debug(`Session Limit Information - Total: ${session_start_limit.total} - Remaining: ${remaining}`); + Total: ${session_start_limit.total} + Remaining: ${remaining}`); } if (!remaining) { this.debug(`Exceeded identify threshold. Will attempt a connection in ${resetAfter}ms`); @@ -396,45 +396,37 @@ class WebSocketManager extends EventEmitter { /** * Checks whether the client is ready to be marked as ready. - * @returns {boolean} * @private */ - checkReady() { + async checkShardsReady() { + if (this.status === Status.READY) return; if (this.shards.size !== this.totalShards || this.shards.some(s => s.status !== Status.READY)) { - return false; + return; } - const unavailableGuilds = this.client.guilds.reduce((acc, guild) => guild.available ? acc : acc + 1, 0); + this.status = Status.NEARLY; - // TODO: Rethink implementation for this - if (unavailableGuilds === 0) { - this.status = Status.NEARLY; - if (!this.client.options.fetchAllMembers) return this.triggerReady(); - // Fetch all members before marking self as ready - const promises = this.client.guilds.map(g => g.members.fetch()); - Promise.all(promises) - .then(() => this.triggerReady()) - .catch(e => { - this.debug(`Failed to fetch all members before ready! ${e}\n${e.stack}`); - this.triggerReady(); + if (this.client.options.fetchAllMembers) { + try { + const promises = this.client.guilds.map(guild => { + if (guild.available) return guild.members.fetch(); + // Return empty promise if guild is unavailable + return Promise.resolve(); }); - } else { - this.debug(`There are ${unavailableGuilds} unavailable guilds. Waiting for their GUILD_CREATE packets`); + await Promise.all(promises); + } catch (err) { + this.debug(`Failed to fetch all members before ready! ${err}\n${err.stack}`); + } } - return true; + this.triggerClientReady(); } /** * Causes the client to be marked as ready and emits the ready event. * @private */ - triggerReady() { - if (this.status === Status.READY) { - this.debug('Tried to mark self as ready, but already ready'); - return; - } - + triggerClientReady() { this.status = Status.READY; this.client.readyAt = new Date(); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index f77635b0f..8d9afd313 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -4,23 +4,15 @@ const EventEmitter = require('events'); const WebSocket = require('../../WebSocket'); const { browser, Status, Events, ShardEvents, OPCodes, WSEvents } = require('../../util/Constants'); -let zstd; +const STATUS_KEYS = Object.keys(Status); +const CONNECTION_STATE = Object.keys(WebSocket.WebSocket); + let zlib; -if (browser) { - zlib = require('pako'); -} else { +if (!browser) { try { - zstd = require('zucc'); - if (!zstd.DecompressStream) zstd = null; - } catch (e) { - try { - zlib = require('zlib-sync'); - if (!zlib.Inflate) zlib = require('pako'); - } catch (err) { - zlib = require('pako'); - } - } + zlib = require('zlib-sync'); + } catch {} // eslint-disable-line no-empty } /** @@ -70,10 +62,10 @@ class WebSocketShard extends EventEmitter { this.sessionID = undefined; /** - * The previous 3 heartbeat pings of the shard (most recent first) - * @type {number[]} + * The previous heartbeat ping of the shard + * @type {number} */ - this.pings = []; + this.ping = -1; /** * The last time a ping was sent (a timestamp) @@ -128,7 +120,7 @@ class WebSocketShard extends EventEmitter { * @type {?NodeJS.Timer} * @private */ - Object.defineProperty(this, 'helloTimeout', { value: null, writable: true }); + Object.defineProperty(this, 'helloTimeout', { value: undefined, writable: true }); /** * If the manager attached its event handlers on the shard @@ -136,16 +128,27 @@ class WebSocketShard extends EventEmitter { * @private */ Object.defineProperty(this, 'eventsAttached', { value: false, writable: true }); - } - /** - * Average heartbeat ping of the websocket, obtained by averaging the WebSocketShard#pings property - * @type {number} - * @readonly - */ - get ping() { - const sum = this.pings.reduce((a, b) => a + b, 0); - return sum / this.pings.length; + /** + * A set of guild IDs this shard expects to receive + * @type {?Set} + * @private + */ + Object.defineProperty(this, 'expectedGuilds', { value: undefined, writable: true }); + + /** + * The ready timeout + * @type {?NodeJS.Timer} + * @private + */ + Object.defineProperty(this, 'readyTimeout', { value: undefined, writable: true }); + + /** + * Time when the WebSocket connection was opened + * @type {number} + * @private + */ + Object.defineProperty(this, 'connectedAt', { value: 0, writable: true }); } /** @@ -166,36 +169,35 @@ class WebSocketShard extends EventEmitter { connect() { const { gateway, client } = this.manager; - if (this.status === Status.READY && this.connection && this.connection.readyState === WebSocket.OPEN) { + if (this.connection && this.connection.readyState === WebSocket.OPEN && this.status === Status.READY) { return Promise.resolve(); } return new Promise((resolve, reject) => { - const onReady = () => { + const cleanup = () => { this.off(ShardEvents.CLOSE, onClose); + this.off(ShardEvents.READY, onReady); this.off(ShardEvents.RESUMED, onResumed); this.off(ShardEvents.INVALID_SESSION, onInvalid); + }; + + const onReady = () => { + cleanup(); resolve(); }; const onResumed = () => { - this.off(ShardEvents.CLOSE, onClose); - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.INVALID_SESSION, onInvalid); + cleanup(); resolve(); }; const onClose = event => { - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.INVALID_SESSION, onInvalid); + cleanup(); reject(event); }; const onInvalid = () => { - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.CLOSE, onClose); + cleanup(); // eslint-disable-next-line prefer-promise-reject-errors reject(); }; @@ -206,29 +208,35 @@ class WebSocketShard extends EventEmitter { this.once(ShardEvents.INVALID_SESSION, onInvalid); if (this.connection && this.connection.readyState === WebSocket.OPEN) { - this.identifyNew(); + this.debug('Connection found, attempting an immediate identify.'); + this.identify(); return; } - if (zstd) { - this.inflate = new zstd.DecompressStream(); - } else { + const wsQuery = { v: client.options.ws.version }; + + if (zlib) { this.inflate = new zlib.Inflate({ chunkSize: 65535, flush: zlib.Z_SYNC_FLUSH, to: WebSocket.encoding === 'json' ? 'string' : '', }); + wsQuery.compress = 'zlib-stream'; } - this.debug(`Trying to connect to ${gateway}, version ${client.options.ws.version}`); + this.debug( + `[CONNECT] + Gateway: ${gateway} + Version: ${client.options.ws.version} + Encoding: ${WebSocket.encoding} + Compression: ${zlib ? 'zlib-stream' : 'none'}`); this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; this.setHelloTimeout(); - const ws = this.connection = WebSocket.create(gateway, { - v: client.options.ws.version, - compress: zstd ? 'zstd-stream' : 'zlib-stream', - }); + this.connectedAt = Date.now(); + + const ws = this.connection = WebSocket.create(gateway, wsQuery); ws.onopen = this.onOpen.bind(this); ws.onmessage = this.onMessage.bind(this); ws.onerror = this.onError.bind(this); @@ -241,7 +249,7 @@ class WebSocketShard extends EventEmitter { * @private */ onOpen() { - this.debug('Opened a connection to the gateway successfully.'); + this.debug(`[CONNECTED] ${this.connection.url} in ${Date.now() - this.connectedAt}ms`); this.status = Status.NEARLY; } @@ -252,10 +260,8 @@ class WebSocketShard extends EventEmitter { */ onMessage({ data }) { let raw; - if (zstd) { - raw = this.inflate.decompress(new Uint8Array(data).buffer); - } else { - if (data instanceof ArrayBuffer) data = new Uint8Array(data); + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + if (zlib) { const l = data.length; const flush = l >= 4 && data[l - 4] === 0x00 && @@ -266,6 +272,8 @@ class WebSocketShard extends EventEmitter { this.inflate.push(data, flush && zlib.Z_SYNC_FLUSH); if (!flush) return; raw = this.inflate.result; + } else { + raw = data; } let packet; try { @@ -281,19 +289,13 @@ class WebSocketShard extends EventEmitter { /** * Called whenever an error occurs with the WebSocket. - * @param {ErrorEvent|Object} event The error that occurred + * @param {ErrorEvent} event The error that occurred * @private */ onError(event) { const error = event && event.error ? event.error : event; if (!error) return; - if (error.message === 'uWs client connection error') { - this.debug('Received a uWs error. Closing the connection and reconnecting...'); - this.connection.close(4000); - return; - } - /** * Emitted whenever a shard's WebSocket encounters a connection error. * @event Client#shardError @@ -324,13 +326,13 @@ class WebSocketShard extends EventEmitter { * @private */ onClose(event) { - this.closeSequence = this.sequence; + if (this.sequence !== -1) this.closeSequence = this.sequence; this.sequence = -1; - this.debug(`WebSocket was closed. - Event Code: ${event.code} - Clean: ${event.wasClean} - Reason: ${event.reason || 'No reason received'}`); + this.debug(`[CLOSE] + Event Code: ${event.code} + Clean: ${event.wasClean} + Reason: ${event.reason || 'No reason received'}`); this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); @@ -360,16 +362,18 @@ class WebSocketShard extends EventEmitter { switch (packet.t) { case WSEvents.READY: /** - * Emitted when the shard becomes ready + * Emitted when the shard receives the READY payload and is now waiting for guilds * @event WebSocketShard#ready */ this.emit(ShardEvents.READY); this.sessionID = packet.d.session_id; - this.status = Status.READY; - this.debug(`READY | Session ${this.sessionID}.`); + this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id)); + this.status = Status.WAITING_FOR_GUILDS; + this.debug(`[READY] Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; - this.sendHeartbeat(); + this.sendHeartbeat('ReadyHeartbeat'); + this.checkReady(); break; case WSEvents.RESUMED: { /** @@ -380,9 +384,10 @@ class WebSocketShard extends EventEmitter { this.status = Status.READY; const replayed = packet.s - this.closeSequence; - this.debug(`RESUMED | Session ${this.sessionID} | Replayed ${replayed} events.`); + this.debug(`[RESUMED] Session ${this.sessionID} | Replayed ${replayed} events.`); this.lastHeartbeatAcked = true; - this.sendHeartbeat(); + this.sendHeartbeat('ResumeHeartbeat'); + break; } } @@ -398,7 +403,7 @@ class WebSocketShard extends EventEmitter { this.connection.close(1001); break; case OPCodes.INVALID_SESSION: - this.debug(`Session invalidated. Resumable: ${packet.d}.`); + this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); // If we can resume the session, do so immediately if (packet.d) { this.identifyResume(); @@ -417,13 +422,56 @@ class WebSocketShard extends EventEmitter { this.ackHeartbeat(); break; case OPCodes.HEARTBEAT: - this.sendHeartbeat(); + this.sendHeartbeat('HeartbeatRequest'); break; default: this.manager.handlePacket(packet, this); + if (this.status === Status.WAITING_FOR_GUILDS && packet.t === WSEvents.GUILD_CREATE) { + this.expectedGuilds.delete(packet.d.id); + this.checkReady(); + } } } + /** + * Checks if the shard can be marked as ready + * @private + */ + checkReady() { + // Step 0. Clear the ready timeout, if it exists + if (this.readyTimeout) { + this.manager.client.clearTimeout(this.readyTimeout); + this.readyTimeout = undefined; + } + // Step 1. If we don't have any other guilds pending, we are ready + if (!this.expectedGuilds.size) { + this.debug('Shard received all its guilds. Marking as fully ready.'); + this.status = Status.READY; + + /** + * Emitted when the shard is fully ready. + * This event is emitted if: + * * all guilds were received by this shard + * * the ready timeout expired, and some guilds are unavailable + * @event WebSocketShard#allReady + * @param {?Set} unavailableGuilds Set of unavailable guilds, if any + */ + this.emit(ShardEvents.ALL_READY); + return; + } + // Step 2. Create a 15s timeout that will mark the shard as ready if there are still unavailable guilds + this.readyTimeout = this.manager.client.setTimeout(() => { + this.debug(`Shard did not receive any more guild packets in 15 seconds. + Unavailable guild count: ${this.expectedGuilds.size}`); + + this.readyTimeout = undefined; + + this.status = Status.READY; + + this.emit(ShardEvents.ALL_READY, this.expectedGuilds); + }, 15000); + } + /** * Sets the HELLO packet timeout. * @param {number} [time] If set to -1, it will clear the hello timeout timeout @@ -434,7 +482,7 @@ class WebSocketShard extends EventEmitter { if (this.helloTimeout) { this.debug('Clearing the HELLO timeout.'); this.manager.client.clearTimeout(this.helloTimeout); - this.helloTimeout = null; + this.helloTimeout = undefined; } return; } @@ -455,26 +503,39 @@ class WebSocketShard extends EventEmitter { if (this.heartbeatInterval) { this.debug('Clearing the heartbeat interval.'); this.manager.client.clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; + this.heartbeatInterval = undefined; } return; } this.debug(`Setting a heartbeat interval for ${time}ms.`); + // Sanity checks + if (this.heartbeatInterval) this.manager.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = this.manager.client.setInterval(() => this.sendHeartbeat(), time); } /** * Sends a heartbeat to the WebSocket. * If this shard didn't receive a heartbeat last time, it will destroy it and reconnect + * @param {string} [tag='HeartbeatTimer'] What caused this heartbeat to be sent + * @param {boolean} [ignoreHeartbeatAck] If we should send the heartbeat forcefully. * @private */ - sendHeartbeat() { - if (!this.lastHeartbeatAcked) { - this.debug("Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting."); + sendHeartbeat(tag = 'HeartbeatTimer', + ignoreHeartbeatAck = [Status.WAITING_FOR_GUILDS, Status.IDENTIFYING, Status.RESUMING].includes(this.status)) { + if (ignoreHeartbeatAck && !this.lastHeartbeatAcked) { + this.debug(`[${tag}] Didn't process heartbeat ack yet but we are still connected. Sending one now.`); + } else if (!this.lastHeartbeatAcked) { + this.debug( + `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. + Status : ${STATUS_KEYS[this.status]} + Sequence : ${this.sequence} + Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` + ); this.destroy(4009); return; } - this.debug('Sending a heartbeat.'); + + this.debug(`[${tag}] Sending a heartbeat.`); this.lastHeartbeatAcked = false; this.lastPingTimestamp = Date.now(); this.send({ op: OPCodes.HEARTBEAT, d: this.sequence }, true); @@ -488,8 +549,7 @@ class WebSocketShard extends EventEmitter { this.lastHeartbeatAcked = true; const latency = Date.now() - this.lastPingTimestamp; this.debug(`Heartbeat acknowledged, latency of ${latency}ms.`); - this.pings.unshift(latency); - if (this.pings.length > 3) this.pings.length = 3; + this.ping = latency; } /** @@ -508,18 +568,20 @@ class WebSocketShard extends EventEmitter { identifyNew() { const { client } = this.manager; if (!client.token) { - this.debug('No token available to identify a new session.'); + this.debug('[IDENTIFY] No token available to identify a new session.'); return; } + this.status = Status.IDENTIFYING; + // Clone the identify payload and assign the token and shard info const d = { ...client.options.ws, token: client.token, - shard: [this.id, Number(client.options.totalShardCount)], + shard: [this.id, Number(client.options.shardCount)], }; - this.debug(`Identifying as a new session. Shard ${this.id}/${client.options.totalShardCount}`); + this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount}`); this.send({ op: OPCodes.IDENTIFY, d }, true); } @@ -529,12 +591,14 @@ class WebSocketShard extends EventEmitter { */ identifyResume() { if (!this.sessionID) { - this.debug('Warning: attempted to resume but no session ID was present; identifying as a new session.'); + this.debug('[RESUME] No session ID was present; identifying as a new session.'); this.identifyNew(); return; } - this.debug(`Attempting to resume session ${this.sessionID} at sequence ${this.closeSequence}`); + this.status = Status.RESUMING; + + this.debug(`[RESUME] Session ${this.sessionID}, sequence ${this.closeSequence}`); const d = { token: this.manager.client.token, @@ -600,15 +664,17 @@ class WebSocketShard extends EventEmitter { /** * Destroys this shard and closes its WebSocket connection. * @param {number} [closeCode=1000] The close code to use + * @param {boolean} [cleanup=false] If the shard should attempt a reconnect * @private */ - destroy(closeCode = 1000) { + destroy(closeCode = 1000, cleanup = false) { this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); + // Close the WebSocket connection, if any - if (this.connection && this.connection.readyState !== WebSocket.CLOSED) { + if (this.connection && this.connection.readyState === WebSocket.OPEN) { this.connection.close(closeCode); - } else { + } else if (!cleanup) { /** * Emitted when a shard is destroyed, but no WebSocket connection was present. * @private @@ -616,9 +682,11 @@ class WebSocketShard extends EventEmitter { */ this.emit(ShardEvents.DESTROYED); } + this.connection = null; // Set the shard status this.status = Status.DISCONNECTED; + if (this.sequence !== -1) this.closeSequence = this.sequence; // Reset the sequence this.sequence = -1; // Reset the ratelimit data diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js index 4fa89edcf..cbb60e13c 100644 --- a/src/client/websocket/handlers/GUILD_BAN_ADD.js +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -4,7 +4,7 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { const guild = client.guilds.get(data.guild_id); - const user = client.users.get(data.user.id); + const user = client.users.add(data.user); /** * Emitted whenever a member is banned from a guild. diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 9f13c6581..33cc0c239 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -8,20 +8,28 @@ module.exports = async (client, { d: data }, shard) => { if (!guild.available && !data.unavailable) { // A newly available guild guild._patch(data); - client.ws.checkReady(); + // If the client was ready before and we had unavailable guilds, fetch them + if (client.ws.status === Status.READY && client.options.fetchAllMembers) { + await guild.members.fetch().catch(err => + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + ); + } } } else { // A new guild data.shardID = shard.id; guild = client.guilds.add(data); - const emitEvent = client.ws.status === Status.READY; - if (emitEvent) { + if (client.ws.status === Status.READY) { /** * Emitted whenever the client joins a guild. * @event Client#guildCreate * @param {Guild} guild The created guild */ - if (client.options.fetchAllMembers) await guild.members.fetch(); + if (client.options.fetchAllMembers) { + await guild.members.fetch().catch(err => + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + ); + } client.emit(Events.GUILD_CREATE, guild); } } diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 039f8f237..1612cfa02 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -16,6 +16,4 @@ module.exports = (client, { d: data }, shard) => { guild.shardID = shard.id; client.guilds.add(guild); } - - client.ws.checkReady(); }; diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 6533b4f83..04d224db4 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -55,7 +55,7 @@ class Shard extends EventEmitter { this.env = Object.assign({}, process.env, { SHARDING_MANAGER: true, SHARDS: this.id, - TOTAL_SHARD_COUNT: this.manager.totalShards, + SHARD_COUNT: this.manager.totalShards, DISCORD_TOKEN: this.manager.token, }); diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 4d0b4a43d..8ed1b9787 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -60,7 +60,7 @@ class ShardClientUtil { * @readonly */ get count() { - return this.client.options.totalShardCount; + return this.client.options.shardCount; } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 12ddfbf8f..06f12f36c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -7,9 +7,10 @@ const browser = exports.browser = typeof window !== 'undefined'; /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number|number[]} [shards] ID of the shard to run, or an array of shard IDs - * @property {number} [shardCount=1] Total number of shards that will be spawned by this Client - * @property {number} [totalShardCount=1] The total amount of shards used by all processes of this bot + * @property {number|number[]|string} [shards] ID of the shard to run, or an array of shard IDs. If not specified, + * the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the + * recommended amount of shards from Discord and spawn that amount + * @property {number} [shardCount=1] The total amount of shards used by all processes of this bot * (e.g. recommended shard count, shard count of the ShardingManager) * @property {number} [messageCacheMaxSize=200] Maximum number of messages to cache per channel * (-1 or Infinity for unlimited - don't do this without message sweeping, otherwise memory usage will climb @@ -42,7 +43,6 @@ const browser = exports.browser = typeof window !== 'undefined'; */ exports.DefaultOptions = { shardCount: 1, - totalShardCount: 1, messageCacheMaxSize: 200, messageCacheLifetime: 0, messageSweepInterval: 0, @@ -163,6 +163,9 @@ exports.Endpoints = { * * IDLE: 3 * * NEARLY: 4 * * DISCONNECTED: 5 + * * WAITING_FOR_GUILDS: 6 + * * IDENTIFYING: 7 + * * RESUMING: 8 * @typedef {number} Status */ exports.Status = { @@ -172,6 +175,9 @@ exports.Status = { IDLE: 3, NEARLY: 4, DISCONNECTED: 5, + WAITING_FOR_GUILDS: 6, + IDENTIFYING: 7, + RESUMING: 8, }; /** @@ -279,6 +285,7 @@ exports.ShardEvents = { INVALID_SESSION: 'invalidSession', READY: 'ready', RESUMED: 'resumed', + ALL_READY: 'allReady', }; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 9eb0b4532..27898c189 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1650,6 +1650,7 @@ declare module 'discord.js' { public on(event: WSEventType, listener: (data: any, shardID: number) => void): this; public once(event: WSEventType, listener: (data: any, shardID: number) => void): this; + private debug(message: string, shard?: WebSocketShard): void; private connect(): Promise; private createShards(): Promise; @@ -1657,9 +1658,9 @@ declare module 'discord.js' { private broadcast(packet: object): void; private destroy(): void; private _handleSessionLimit(remaining?: number, resetAfter?: number): Promise; - private handlePacket(packet?: object, shard?: WebSocketShard): Promise; - private checkReady(): boolean; - private triggerReady(): void; + private handlePacket(packet?: object, shard?: WebSocketShard): boolean; + private checkShardsReady(): Promise; + private triggerClientReady(): void; } export class WebSocketShard extends EventEmitter { @@ -1671,14 +1672,15 @@ declare module 'discord.js' { private lastHeartbeatAcked: boolean; private ratelimit: { queue: object[]; total: number; remaining: number; time: 60e3; timer: NodeJS.Timeout | null; }; private connection: WebSocket | null; - private helloTimeout: NodeJS.Timeout | null; + private helloTimeout: NodeJS.Timeout | undefined; private eventsAttached: boolean; + private expectedGuilds: Set | undefined; + private readyTimeout: NodeJS.Timeout | undefined; public manager: WebSocketManager; public id: number; public status: Status; - public pings: [number, number, number]; - public readonly ping: number; + public ping: number; private debug(message: string): void; private connect(): Promise; @@ -1687,6 +1689,7 @@ declare module 'discord.js' { private onError(error: ErrorEvent | object): void; private onClose(event: CloseEvent): void; private onPacket(packet: object): void; + private checkReady(): void; private setHelloTimeout(time?: number): void; private setHeartbeatTimer(time: number): void; private sendHeartbeat(): void; @@ -1703,12 +1706,14 @@ declare module 'discord.js' { public on(event: 'resumed', listener: () => void): this; public on(event: 'close', listener: (event: CloseEvent) => void): this; public on(event: 'invalidSession', listener: () => void): this; + public on(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; public on(event: string, listener: Function): this; public once(event: 'ready', listener: () => void): this; public once(event: 'resumed', listener: () => void): this; public once(event: 'close', listener: (event: CloseEvent) => void): this; public once(event: 'invalidSession', listener: () => void): this; + public once(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; public once(event: string, listener: Function): this; } @@ -2054,9 +2059,8 @@ declare module 'discord.js' { } interface ClientOptions { - shards?: number | number[]; - shardCount?: number | 'auto'; - totalShardCount?: number; + shards?: number | number[] | 'auto'; + shardCount?: number; messageCacheMaxSize?: number; messageCacheLifetime?: number; messageSweepInterval?: number; From 7d74e7e4196b42e45b57c6c20461664468bd8e43 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 20 Dec 2019 11:58:46 +0100 Subject: [PATCH 1229/1359] typings(Extendable): add missing channels (#3581) --- typings/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 27898c189..c8400d184 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2148,6 +2148,8 @@ declare module 'discord.js' { TextChannel: typeof TextChannel; VoiceChannel: typeof VoiceChannel; CategoryChannel: typeof CategoryChannel; + NewsChannel: typeof NewsChannel; + StoreChannel: typeof StoreChannel; GuildMember: typeof GuildMember; Guild: typeof Guild; Message: typeof Message; From 99e8d3c540ae2c54effafb945acdd22b5241e58d Mon Sep 17 00:00:00 2001 From: Sugden Date: Sat, 21 Dec 2019 19:32:24 +0000 Subject: [PATCH 1230/1359] cleanup: remove acknowledge method from TextChannel & DMChannel (#3635) * Update TextChannel.js * remove acknowledge method --- src/structures/DMChannel.js | 1 - src/structures/TextChannel.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index e172f2260..02dca0d19 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -83,7 +83,6 @@ class DMChannel extends Channel { createMessageCollector() {} awaitMessages() {} // Doesn't work on DM channels; bulkDelete() {} - acknowledge() {} _cacheMessage() {} } diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 55c3b0d24..66212e70a 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -138,7 +138,6 @@ class TextChannel extends GuildChannel { createMessageCollector() {} awaitMessages() {} bulkDelete() {} - acknowledge() {} } TextBasedChannel.applyToClass(TextChannel, true); From e13b3f550dd550b5c3fb8ae211a456d330a7768f Mon Sep 17 00:00:00 2001 From: Charlie Date: Sat, 21 Dec 2019 21:12:35 +0100 Subject: [PATCH 1231/1359] typings: TextChannel.topic & NewsChannel.topic should be nullable (#3628) * Fix GuildChannel#topic to be optional * Update typings/index.d.ts Implement the suggested change from optional to null return Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> * Update typings/index.d.ts Implement the suggested change from optional to null return Co-Authored-By: izexi <43889168+izexi@users.noreply.github.com> Co-authored-by: izexi <43889168+izexi@users.noreply.github.com> --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c8400d184..d26160feb 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1373,7 +1373,7 @@ declare module 'discord.js' { public messages: MessageStore; public nsfw: boolean; public rateLimitPerUser: number; - public topic: string; + public topic: string | null; public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; @@ -1384,7 +1384,7 @@ declare module 'discord.js' { constructor(guild: Guild, data?: object); public messages: MessageStore; public nsfw: boolean; - public topic: string; + public topic: string | null; public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public fetchWebhooks(): Promise>; From f578cce9ac75114c51f7a4538974817a3836d3fd Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Sat, 21 Dec 2019 12:27:14 -0800 Subject: [PATCH 1232/1359] feat(Guild): add systemChannelFlags (#3559) * Add systemChannelFlags bitfield to Guild * Implement @vladfrangu's suggestions * fix: apply suggestions, reverse order of flags, reword docs * docs: add SystemCHannelFlagsResolvable typedef Co-authored-by: SpaceEEC --- src/index.js | 1 + src/structures/Guild.js | 21 +++++++++++++++++++++ src/util/SystemChannelFlags.js | 33 +++++++++++++++++++++++++++++++++ typings/index.d.ts | 13 +++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/util/SystemChannelFlags.js diff --git a/src/index.js b/src/index.js index 2d776e7ca..cbc0d5a79 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,7 @@ module.exports = { Snowflake: require('./util/Snowflake'), SnowflakeUtil: require('./util/Snowflake'), Structures: require('./util/Structures'), + SystemChannelFlags: require('./util/SystemChannelFlags'), Util: Util, util: Util, version: require('../package.json').version, diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 75925663e..987fa38f2 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -10,6 +10,7 @@ const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Snowflake = require('../util/Snowflake'); +const SystemChannelFlags = require('../util/SystemChannelFlags'); const GuildMemberStore = require('../stores/GuildMemberStore'); const RoleStore = require('../stores/RoleStore'); const GuildEmojiStore = require('../stores/GuildEmojiStore'); @@ -275,6 +276,12 @@ class Guild extends Base { this.defaultMessageNotifications = DefaultMessageNotifications[data.default_message_notifications] || data.default_message_notifications; + /** + * The value set for the guild's system channel flags + * @type {Readonly} + */ + this.systemChannelFlags = new SystemChannelFlags(data.system_channel_flags).freeze(); + /** * The maximum amount of members the guild can have * You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter @@ -773,6 +780,7 @@ class Guild extends Base { * @property {Base64Resolvable} [splash] The splash screen of the guild * @property {Base64Resolvable} [banner] The banner of the guild * @property {DefaultMessageNotifications|number} [defaultMessageNotifications] The default message notifications + * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild */ /** @@ -813,6 +821,9 @@ class Guild extends Base { DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) : Number(data.defaultMessageNotifications); } + if (typeof data.systemChannelFlags !== 'undefined') { + _data.systemChannelFlags = SystemChannelFlags.resolve(data.systemChannelFlags); + } return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); } @@ -839,6 +850,16 @@ class Guild extends Base { } /* eslint-enable max-len */ + /** + * Edits the flags of the default message notifications of the guild. + * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications + * @param {string} [reason] Reason for changing the flags of the default message notifications + * @returns {Promise} + */ + setSystemChannelFlags(systemChannelFlags, reason) { + return this.edit({ systemChannelFlags }, reason); + } + /** * Edits the name of the guild. * @param {string} name The new name of the guild diff --git a/src/util/SystemChannelFlags.js b/src/util/SystemChannelFlags.js new file mode 100644 index 000000000..14e8fd4f4 --- /dev/null +++ b/src/util/SystemChannelFlags.js @@ -0,0 +1,33 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link Guild#systemChannelFlags} bitfield. + * Note that all event message types are enabled by default, + * and by setting their corresponding flags you are disabling them + * @extends {BitField} + */ +class SystemChannelFlags extends BitField { + /** + * Data that can be resolved to give a sytem channel flag bitfield. This can be: + * * A string (see {@link SystemChannelFlags.FLAGS}) + * * A sytem channel flag + * * An instance of SystemChannelFlags + * * An Array of SystemChannelFlagsResolvable + * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable + */ +} + +/** + * Numeric system channel flags. All available properties: + * * `WELCOME_MESSAGE_DISABLED` + * * `BOOST_MESSAGE_DISABLED` + * @type {Object} + */ +SystemChannelFlags.FLAGS = { + WELCOME_MESSAGE_DISABLED: 1 << 0, + BOOST_MESSAGE_DISABLED: 1 << 1, +}; + +module.exports = SystemChannelFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index d26160feb..7ab448e7d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -714,6 +714,7 @@ declare module 'discord.js' { public shardID: number; public splash: string | null; public readonly systemChannel: TextChannel | null; + public systemChannelFlags: Readonly; public systemChannelID: Snowflake | null; public vanityURLCode: string | null; public verificationLevel: number; @@ -755,6 +756,7 @@ declare module 'discord.js' { public setRolePositions(rolePositions: RolePosition[]): Promise; public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; + public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; public splashURL(options?: AvatarOptions): string | null; public toJSON(): object; @@ -1368,6 +1370,11 @@ declare module 'discord.js' { static extend(structure: string, extender: (baseClass: typeof Function) => T): T; } + export class SystemChannelFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); public messages: MessageStore; @@ -2285,6 +2292,7 @@ declare module 'discord.js' { defaultMessageNotifications?: DefaultMessageNotifications | number; afkChannel?: ChannelResolvable; systemChannel?: ChannelResolvable; + systemChannelFlags?: SystemChannelFlags; afkTimeout?: number; icon?: Base64Resolvable; owner?: GuildMemberResolvable; @@ -2613,6 +2621,11 @@ declare module 'discord.js' { type StringResolvable = string | string[] | any; + type SystemChannelFlagsString = 'WELCOME_MESSAGE_DISABLED' + | 'BOOST_MESSAGE_DISABLED'; + + type SystemChannelFlagsResolvable = BitFieldResolvable; + type TargetUser = number; type UserResolvable = User | Snowflake | Message | GuildMember; From b4f00bfb6be6e6db25ca33122bd6e4318f081bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Sat, 21 Dec 2019 21:28:09 +0100 Subject: [PATCH 1233/1359] feat: widen GuildResolvable to include more structures (#3512) * feat: Widen GuildResolvable to include GuildChannel and GuildMember * docs: Documented the new overloads Co-Authored-By: Gryffon Bellish Co-authored-by: Gryffon Bellish --- src/stores/GuildStore.js | 19 ++++++++++++++++++- typings/index.d.ts | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 90e4358dd..eb7090b63 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -4,6 +4,9 @@ const DataStore = require('./DataStore'); const DataResolver = require('../util/DataResolver'); const { Events } = require('../util/Constants'); const Guild = require('../structures/Guild'); +const GuildChannel = require('../structures/GuildChannel'); +const GuildMember = require('../structures/GuildMember'); +const Role = require('../structures/Role'); /** * Stores guilds. @@ -17,8 +20,10 @@ class GuildStore extends DataStore { /** * Data that resolves to give a Guild object. This can be: * * A Guild object + * * A GuildChannel object + * * A Role object * * A Snowflake - * @typedef {Guild|Snowflake} GuildResolvable + * @typedef {Guild|GuildChannel|GuildMember|Role|Snowflake} GuildResolvable */ /** @@ -29,6 +34,12 @@ class GuildStore extends DataStore { * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Guild} */ + resolve(guild) { + if (guild instanceof GuildChannel || + guild instanceof GuildMember || + guild instanceof Role) return super.resolve(guild.guild); + return super.resolve(guild); + } /** * Resolves a GuildResolvable to a Guild ID string. @@ -38,6 +49,12 @@ class GuildStore extends DataStore { * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Snowflake} */ + resolveID(guild) { + if (guild instanceof GuildChannel || + guild instanceof GuildMember || + guild instanceof Role) return super.resolveID(guild.guild.id); + return super.resolveID(guild); + } /** * Creates a guild. diff --git a/typings/index.d.ts b/typings/index.d.ts index 7ab448e7d..348a7ba96 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2328,7 +2328,7 @@ declare module 'discord.js' { type GuildMemberResolvable = GuildMember | UserResolvable; - type GuildResolvable = Guild | Snowflake; + type GuildResolvable = Guild | GuildChannel | GuildMember | Role | Snowflake; interface GuildPruneMembersOptions { count?: boolean; From 710101c5805935d7804fc8e53fa130541f248c54 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 22 Dec 2019 11:31:26 +0200 Subject: [PATCH 1234/1359] src(WebSocket): fix race condition (#3636) A race condition caused Client#user to be null in the ready event if the client handled 0 guilds. --- src/client/websocket/WebSocketShard.js | 1 - src/client/websocket/handlers/READY.js | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 8d9afd313..5171d7e59 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -373,7 +373,6 @@ class WebSocketShard extends EventEmitter { this.debug(`[READY] Session ${this.sessionID}.`); this.lastHeartbeatAcked = true; this.sendHeartbeat('ReadyHeartbeat'); - this.checkReady(); break; case WSEvents.RESUMED: { /** diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 1612cfa02..002575023 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -16,4 +16,6 @@ module.exports = (client, { d: data }, shard) => { guild.shardID = shard.id; client.guilds.add(guild); } + + shard.checkReady(); }; From 45b89710008d207da22000dcb633c0c0236db17e Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Mon, 23 Dec 2019 17:01:07 -0500 Subject: [PATCH 1235/1359] deps: mark utf-8-validate as optional, remove mentions of uws and zucc (#3638) * Mark utf-8-validate as optional * remove uws and zucc --- package.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c8decaa3b..6522e2f45 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,6 @@ "zlib-sync": "^0.1.6" }, "peerDependenciesMeta": { - "@discordjs/uws": { - "optional": true - }, "bufferutil": { "optional": true }, @@ -68,10 +65,10 @@ "sodium": { "optional": true }, - "zlib-sync": { + "utf-8-validate": { "optional": true }, - "zucc": { + "zlib-sync": { "optional": true } }, From d4333f5bbe0aff3557ac51b7acb8ec62eb927a47 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Tue, 24 Dec 2019 21:28:09 -0500 Subject: [PATCH 1236/1359] chore: node version in package.json (#3643) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6522e2f45..a52ade25f 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "webpack-cli": "^3.2.3" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" }, "browser": { "https": false, From 50ed3293a57aeb0f9c094f00d49ed9874dc5d87a Mon Sep 17 00:00:00 2001 From: NightScript Date: Tue, 24 Dec 2019 18:29:19 -0800 Subject: [PATCH 1237/1359] chore: issue config refactor (#3640) * Create config.yml Instead of making an entire new page with just text talking about the discord server (which they could ignore, as most people don't read), just link people directly to the discord server * Delete question---general-support-request.md --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ .../question---general-support-request.md | 13 ------------- 2 files changed, 5 insertions(+), 13 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/question---general-support-request.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..2584693cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: discord.js discord server + url: https://discord.gg/bRCvFy9 + about: Please use this Discord Server to ask questions and get support. We don't typically answer questions here and they will likely be closed and redirected to the Discord server. diff --git a/.github/ISSUE_TEMPLATE/question---general-support-request.md b/.github/ISSUE_TEMPLATE/question---general-support-request.md deleted file mode 100644 index d48d49400..000000000 --- a/.github/ISSUE_TEMPLATE/question---general-support-request.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Question / General support request -about: Ask for help in Discord instead - https://discord.gg/bRCvFy9 -title: '' -labels: question (please use Discord instead) -assignees: '' - ---- - -Seriously, we only use this issue tracker for bugs in the library itself and feature requests for it. -We don't typically answer questions or help with support issues here. - -If you have a question or need support on our library, please read our [Support Document](https://github.com/discordjs/discord.js/blob/master/.github/SUPPORT.md) From fc27ce1a1533a3c412bcdd9b1ad25ef40b11f544 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:26:41 +0100 Subject: [PATCH 1238/1359] typings(Bitfield): add hasParams to toArray, fix serialize's type (#3579) * typings(Bitfield): add hasParams to toArray, fix serialize's type * fix: apply suggested changes * chore: remove incorrect whitespace * fix: make params optional * nit: pluralize bit in Permissions#missing * nit: group non-static methods together --- typings/index.d.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 348a7ba96..ed0882269 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -118,10 +118,10 @@ declare module 'discord.js' { public equals(bit: BitFieldResolvable): boolean; public freeze(): Readonly>; public has(bit: BitFieldResolvable): boolean; - public missing(bits: BitFieldResolvable, ...hasParams: any[]): S[]; + public missing(bits: BitFieldResolvable, ...hasParam: readonly unknown[]): S[]; public remove(...bits: BitFieldResolvable[]): BitField; - public serialize(...hasParams: BitFieldResolvable[]): Record; - public toArray(): S[]; + public serialize(...hasParam: readonly unknown[]): Record; + public toArray(...hasParam: readonly unknown[]): S[]; public toJSON(): number; public valueOf(): number; public [Symbol.iterator](): IterableIterator; @@ -1124,6 +1124,9 @@ declare module 'discord.js' { export class Permissions extends BitField { public any(permission: PermissionResolvable, checkAdmin?: boolean): boolean; public has(permission: PermissionResolvable, checkAdmin?: boolean): boolean; + public missing(bits: BitFieldResolvable, checkAdmin?: boolean): PermissionString[]; + public serialize(checkAdmin?: boolean): Record; + public toArray(checkAdmin?: boolean): PermissionString[]; public static ALL: number; public static DEFAULT: number; From ea76a5663937afe78b58076eefb6ab1ed2fc1795 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:27:34 +0100 Subject: [PATCH 1239/1359] feat(Webhook): add type property and created* getters (#3585) * feat(Webhook): add created* getters * feat(Webhook): add type property * typings(WebhookFields): use primitive string for url getter Co-Authored-By: Gryffon Bellish * fix(Webhook): token can be null Co-authored-by: Gryffon Bellish --- src/structures/Webhook.js | 32 ++++++++++++++++++++++++++++++-- src/util/Constants.js | 13 +++++++++++++ typings/index.d.ts | 10 ++++++++-- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index da7dcd12f..b4c228e97 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,6 +1,8 @@ 'use strict'; +const { WebhookTypes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); +const Snowflake = require('../util/Snowflake'); const Channel = require('./Channel'); const APIMessage = require('./APIMessage'); @@ -29,9 +31,9 @@ class Webhook { /** * The token for the webhook * @name Webhook#token - * @type {string} + * @type {?string} */ - Object.defineProperty(this, 'token', { value: data.token, writable: true, configurable: true }); + Object.defineProperty(this, 'token', { value: data.token || null, writable: true, configurable: true }); /** * The avatar for the webhook @@ -45,6 +47,12 @@ class Webhook { */ this.id = data.id; + /** + * The type of the webhook + * @type {WebhookTypes} + */ + this.type = WebhookTypes[data.type]; + /** * The guild the webhook belongs to * @type {Snowflake} @@ -210,6 +218,23 @@ class Webhook { delete(reason) { return this.client.api.webhooks(this.id, this.token).delete({ reason }); } + /** + * The timestamp the webhook was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return Snowflake.deconstruct(this.id).timestamp; + } + + /** + * The time the webhook was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } /** * The url of this webhook @@ -236,6 +261,9 @@ class Webhook { 'sendSlackMessage', 'edit', 'delete', + 'createdTimestamp', + 'createdAt', + 'url', ]) { Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(Webhook.prototype, prop)); diff --git a/src/util/Constants.js b/src/util/Constants.js index 06f12f36c..4c2f60590 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -628,6 +628,19 @@ exports.MembershipStates = [ 'ACCEPTED', ]; +/** + * The value set for a webhook's type: + * * Incoming + * * Channel Follower + * @typedef {string} WebhookTypes + */ +exports.WebhookTypes = [ + // They start at 1 + null, + 'Incoming', + 'Channel Follower', +]; + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/typings/index.d.ts b/typings/index.d.ts index ed0882269..de3f707a8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1636,11 +1636,13 @@ declare module 'discord.js' { public guildID: Snowflake; public name: string; public owner: User | object | null; - public readonly url: string; + public token: string | null; + public type: WebhookTypes; } export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); + public token: string; } export class WebSocketManager extends EventEmitter { @@ -1909,7 +1911,9 @@ declare module 'discord.js' { interface WebhookFields { readonly client: Client; id: Snowflake; - token: string; + readonly createdAt: Date; + readonly createdTimestamp: number; + readonly url: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false } | MessageAdditions): Promise; @@ -2654,6 +2658,8 @@ declare module 'discord.js' { split?: boolean | SplitOptions; } + type WebhookTypes = 'Incoming' | 'Channel Follower'; + interface WebSocketOptions { large_threshold?: number; compress?: boolean; From e660ea90cc2553c53dbc702ccb49de26916a9eec Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:27:48 +0100 Subject: [PATCH 1240/1359] fix(Webhook): edit channel when editing avatar (#3588) --- src/structures/Webhook.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index b4c228e97..9b6b43c8f 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -194,20 +194,20 @@ class Webhook { * @param {string} [reason] Reason for editing this webhook * @returns {Promise} */ - edit({ name = this.name, avatar, channel }, reason) { + async edit({ name = this.name, avatar, channel }, reason) { if (avatar && (typeof avatar === 'string' && !avatar.startsWith('data:'))) { - return DataResolver.resolveImage(avatar).then(image => this.edit({ name, avatar: image }, reason)); + avatar = await DataResolver.resolveImage(avatar); } if (channel) channel = channel instanceof Channel ? channel.id : channel; - return this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({ + const data = await this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({ data: { name, avatar, channel_id: channel }, reason, - }).then(data => { - this.name = data.name; - this.avatar = data.avatar; - this.channelID = data.channel_id; - return this; }); + + this.name = data.name; + this.avatar = data.avatar; + this.channelID = data.channel_id; + return this; } /** From 97eac663b3e1292cc399c56eb701069301f4637d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Fri, 27 Dec 2019 19:28:04 +0100 Subject: [PATCH 1241/1359] feat(MessageMentions): cache mentioned members (#3601) --- src/structures/MessageMentions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 9b17685f6..29a029bf7 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -48,6 +48,9 @@ class MessageMentions { } else { this.users = new Collection(); for (const mention of users) { + if (mention.member && message.guild) { + message.guild.members.add(Object.assign(mention.member, { user: mention })); + } const user = message.client.users.add(mention); this.users.set(user.id, user); } From c734979ad4dc4be6999b3b1eef73d55b439c0ae4 Mon Sep 17 00:00:00 2001 From: Cadence Fish Date: Sat, 4 Jan 2020 01:53:27 +1300 Subject: [PATCH 1242/1359] typings(ShardingManager): add options.shardList (#3657) --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index de3f707a8..b731c05e2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1285,6 +1285,7 @@ declare module 'discord.js' { export class ShardingManager extends EventEmitter { constructor(file: string, options?: { totalShards?: number | 'auto'; + shardList?: number[] | 'auto'; mode?: ShardingManagerMode; respawn?: boolean; shardArgs?: string[]; From 155b682f6c9b0be7525141c054257657539b29ae Mon Sep 17 00:00:00 2001 From: Jyguy Date: Sat, 4 Jan 2020 17:23:01 -0500 Subject: [PATCH 1243/1359] typings(GuildEmoji): make url not-nullable (#3656) * typings(GuildEmoji): make url not-nullable * make GuildEmoji.url readonly --- typings/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index b731c05e2..63942fff8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -843,6 +843,7 @@ declare module 'discord.js' { public managed: boolean; public requiresColons: boolean; public roles: GuildEmojiRoleStore; + public readonly url: string; public delete(reason?: string): Promise; public edit(data: GuildEmojiEditData, reason?: string): Promise; public equals(other: GuildEmoji | object): boolean; From bf31b28ad95ce98840aac898209715b2c7b450ad Mon Sep 17 00:00:00 2001 From: tipakA <31581159+tipakA@users.noreply.github.com> Date: Sun, 5 Jan 2020 00:50:38 +0100 Subject: [PATCH 1244/1359] feat(RichPresenceAssets): add Twitch preview link for largeImageURL (#3655) --- src/structures/Presence.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 2eccf7cb2..8c207989f 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -312,6 +312,8 @@ class RichPresenceAssets { if (!this.largeImage) return null; if (/^spotify:/.test(this.largeImage)) { return `https://i.scdn.co/image/${this.largeImage.slice(8)}`; + } else if (/^twitch:/.test(this.largeImage)) { + return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${this.largeImage.slice(7)}.png`; } return this.activity.presence.client.rest.cdn .AppAsset(this.activity.applicationID, this.largeImage, { format, size }); From 6af0da10436b13b29033a1da0a7ea96598651029 Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sun, 5 Jan 2020 15:45:49 +0000 Subject: [PATCH 1245/1359] feat(Partials): add DMChannel/MessageReaction#fetch() and PartialTypes.REACTION (#3474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add DMChannel#fetch() & Action#getChannel({recipients}) * ref for MessageReaction partial * typings * add PartialTypes.REACTION * accommodate for fully removed reactions * fix incorrect wording and typo * typings: MessageReaction#count is nullable * typings: mark MessageReaction#partial as readonly Co-Authored-By: Vlad Frangu * fix(User): fetch dm channel if cached one is partial * docs: add missing comma Co-Authored-By: Antonio Román * fix: accomodate for new reactions * fix: updating existing/new count on _patch * docs: typo * for consistency Co-authored-by: Vlad Frangu Co-authored-by: SpaceEEC Co-authored-by: Antonio Román --- docs/topics/partials.md | 8 ++++-- src/client/actions/Action.js | 5 ++-- src/client/actions/MessageReactionAdd.js | 5 +++- src/stores/ReactionStore.js | 22 +++++++++++++++ src/structures/DMChannel.js | 10 ++++++- src/structures/MessageReaction.js | 35 ++++++++++++++++++++---- src/structures/User.js | 2 +- src/util/Constants.js | 2 ++ typings/index.d.ts | 8 ++++-- 9 files changed, 82 insertions(+), 15 deletions(-) diff --git a/docs/topics/partials.md b/docs/topics/partials.md index f1f05fe28..c930d0fd7 100644 --- a/docs/topics/partials.md +++ b/docs/topics/partials.md @@ -9,8 +9,8 @@ discard the event. With partials, you're able to receive the event, with a Messa Partials are opt-in, and you can enable them in the Client options by specifying [PartialTypes](/#/docs/main/master/typedef/PartialType): ```js -// Accept partial messages and DM channels when emitting events -new Client({ partials: ['MESSAGE', 'CHANNEL'] }); +// Accept partial messages, DM channels, and reactions when emitting events +new Client({ partials: ['MESSAGE', 'CHANNEL', 'REACTION'] }); ``` ## Usage & warnings @@ -45,6 +45,10 @@ client.on('messageReactionAdd', async (reaction, user) => { if (reaction.message.partial) await reaction.message.fetch(); // Now the message has been cached and is fully available: console.log(`${reaction.message.author}'s message "${reaction.message.content}" gained a reaction!`); + // Fetches and caches the reaction itself, updating resources that were possibly defunct. + if (reaction.partial) await reaction.fetch(); + // Now the reaction is fully available and the properties will be reflected accurately: + console.log(`${reaction.count} user(s) have given the same reaction to this message!`); }); ``` diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 7f21318b4..bc9ed267a 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -36,6 +36,7 @@ class GenericAction { return data.channel || this.getPayload({ id, guild_id: data.guild_id, + recipients: [data.author || { id: data.user_id }], }, this.client.channels, id, PartialTypes.CHANNEL); } @@ -52,9 +53,9 @@ class GenericAction { const id = data.emoji.id || decodeURIComponent(data.emoji.name); return this.getPayload({ emoji: data.emoji, - count: 0, + count: message.partial ? null : 0, me: user.id === this.client.user.id, - }, message.reactions, id, PartialTypes.MESSAGE); + }, message.reactions, id, PartialTypes.REACTION); } getMember(data, guild) { diff --git a/src/client/actions/MessageReactionAdd.js b/src/client/actions/MessageReactionAdd.js index e7ae7e26a..22287dbd1 100644 --- a/src/client/actions/MessageReactionAdd.js +++ b/src/client/actions/MessageReactionAdd.js @@ -2,6 +2,7 @@ const Action = require('./Action'); const { Events } = require('../../util/Constants'); +const { PartialTypes } = require('../../util/Constants'); /* { user_id: 'id', @@ -26,11 +27,13 @@ class MessageReactionAdd extends Action { if (!message) return false; // Verify reaction + if (message.partial && !this.client.options.partials.includes(PartialTypes.REACTION)) return false; const reaction = message.reactions.add({ emoji: data.emoji, - count: 0, + count: message.partial ? null : 0, me: user.id === this.client.user.id, }); + if (!reaction) return false; reaction._add(user); /** * Emitted whenever a reaction is added to a cached message. diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 5092e861f..1b1fb6030 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -50,6 +50,28 @@ class ReactionStore extends DataStore { return this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions.delete() .then(() => this.message); } + + _partial(emoji) { + const id = emoji.id || emoji.name; + const existing = this.get(id); + return !existing || existing.partial; + } + + async _fetchReaction(reactionEmoji, cache) { + const id = reactionEmoji.id || reactionEmoji.name; + const existing = this.get(id); + if (!this._partial(reactionEmoji)) return existing; + const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); + if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { + reactionEmoji.reaction._patch({ count: 0 }); + this.message.reactions.remove(id); + return existing; + } + for (const reaction of data.reactions) { + if (this._partial(reaction.emoji)) this.add(reaction, cache); + } + return existing; + } } module.exports = ReactionStore; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 02dca0d19..006a9fab2 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -56,7 +56,15 @@ class DMChannel extends Channel { * @readonly */ get partial() { - return !this.recipient; + return typeof this.lastMessageID === 'undefined'; + } + + /** + * Fetch this DMChannel. + * @returns {Promise} + */ + fetch() { + return this.recipient.createDM(); } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index fe10e428f..f32f0843c 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -27,12 +27,6 @@ class MessageReaction { */ this.me = data.me; - /** - * The number of people that have given the same reaction - * @type {number} - */ - this.count = data.count || 0; - /** * The users that have given this reaction, mapped by their ID * @type {ReactionUserStore} @@ -40,6 +34,17 @@ class MessageReaction { this.users = new ReactionUserStore(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); + + this._patch(data); + } + + _patch(data) { + /** + * The number of people that have given the same reaction + * @type {?number} + */ + // eslint-disable-next-line eqeqeq + if (this.count == undefined) this.count = data.count; } /** @@ -63,18 +68,36 @@ class MessageReaction { return this._emoji; } + /** + * Whether or not this reaction is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.count === null; + } + + /** + * Fetch this reaction. + * @returns {Promise} + */ + fetch() { + return this.message.reactions._fetchReaction(this.emoji, true); + } toJSON() { return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); } _add(user) { + if (this.partial) return; this.users.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { + if (this.partial) return; this.users.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; diff --git a/src/structures/User.js b/src/structures/User.js index b5358be7c..a4b1d3d92 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -218,7 +218,7 @@ class User extends Base { */ async createDM() { const { dmChannel } = this; - if (dmChannel) return dmChannel; + if (dmChannel && !dmChannel.partial) return dmChannel; const data = await this.client.api.users(this.client.user.id).channels.post({ data: { recipient_id: this.id, } }); diff --git a/src/util/Constants.js b/src/util/Constants.js index 4c2f60590..6ef681272 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -294,6 +294,7 @@ exports.ShardEvents = { * * CHANNEL (only affects DMChannels) * * GUILD_MEMBER * * MESSAGE + * * REACTION * Partials require you to put checks in place when handling data, read the Partials topic listed in the * sidebar for more information. * @typedef {string} PartialType @@ -303,6 +304,7 @@ exports.PartialTypes = keyMirror([ 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', + 'REACTION', ]); /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 63942fff8..b44df577b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -649,6 +649,7 @@ declare module 'discord.js' { public messages: MessageStore; public recipient: User; public readonly partial: false; + public fetch(): Promise; } export class Emoji extends Base { @@ -1100,11 +1101,13 @@ declare module 'discord.js' { constructor(client: Client, data: object, message: Message); private _emoji: GuildEmoji | ReactionEmoji; - public count: number; + public count: number | null; public readonly emoji: GuildEmoji | ReactionEmoji; public me: boolean; public message: Message; + public readonly partial: boolean; public users: ReactionUserStore; + public fetch(): Promise; public toJSON(): object; } @@ -2539,7 +2542,8 @@ declare module 'discord.js' { type PartialTypes = 'USER' | 'CHANNEL' | 'GUILD_MEMBER' - | 'MESSAGE'; + | 'MESSAGE' + | 'REACTION'; type Partialize = { id: string; From d2ef02906c5618d883b1adfb348fd248a4db616f Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Sun, 5 Jan 2020 11:45:16 -0500 Subject: [PATCH 1246/1359] cleanup(DataResolver): stats can't be falsy (#3651) --- src/util/DataResolver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js index 91933eb92..b02189168 100644 --- a/src/util/DataResolver.js +++ b/src/util/DataResolver.js @@ -96,7 +96,7 @@ class DataResolver { const file = browser ? resource : path.resolve(resource); fs.stat(file, (err, stats) => { if (err) return reject(err); - if (!stats || !stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); + if (!stats.isFile()) return reject(new DiscordError('FILE_NOT_FOUND', file)); fs.readFile(file, (err2, data) => { if (err2) reject(err2); else resolve(data); From a53d86579b8150de5388eef5d07520b26d3f7d04 Mon Sep 17 00:00:00 2001 From: Saya <36309350+Deivu@users.noreply.github.com> Date: Fri, 10 Jan 2020 06:14:55 +0800 Subject: [PATCH 1247/1359] typings(BaseClient): remove delay parameter from setImmediate (#3667) There is no delay parameter on setImmediate in Node.JS docs: https://nodejs.org/docs/latest-v12.x/api/timers.html#timers_setimmediate_callback_args --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index b44df577b..15cbeee7f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -102,7 +102,7 @@ declare module 'discord.js' { public destroy(): void; public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; - public setImmediate(fn: Function, delay: number, ...args: any[]): NodeJS.Immediate; + public setImmediate(fn: Function, ...args: any[]): NodeJS.Immediate; public toJSON(...props: { [key: string]: boolean | string }[]): object; } From f74ae12d6ac8070ff67a2ba9ff6cd5e3e0cbcc42 Mon Sep 17 00:00:00 2001 From: didinele Date: Sat, 11 Jan 2020 21:42:01 +0200 Subject: [PATCH 1248/1359] fix(typings): remove VoiceChannel#connection (#3676) --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 15cbeee7f..1cd568f4b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1497,7 +1497,6 @@ declare module 'discord.js' { export class VoiceChannel extends GuildChannel { constructor(guild: Guild, data?: object); public bitrate: number; - public readonly connection: VoiceConnection; public readonly editable: boolean; public readonly full: boolean; public readonly joinable: boolean; From b5825c33b0f07b57d174a7d0bf157b19dc99e73c Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 14:58:40 +0100 Subject: [PATCH 1249/1359] feat(Speaking): add PRIORITY_SPEAKING bit (#3680) --- src/util/Speaking.js | 2 ++ typings/index.d.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/Speaking.js b/src/util/Speaking.js index c706d9953..18c1ae6a4 100644 --- a/src/util/Speaking.js +++ b/src/util/Speaking.js @@ -13,12 +13,14 @@ class Speaking extends BitField {} * Numeric speaking flags. All available properties: * * `SPEAKING` * * `SOUNDSHARE` + * * `PRIORITY_SPEAKING` * @type {Object} * @see {@link https://discordapp.com/developers/docs/topics/voice-connections#speaking} */ Speaking.FLAGS = { SPEAKING: 1 << 0, SOUNDSHARE: 1 << 1, + PRIORITY_SPEAKING: 1 << 2, }; module.exports = Speaking; diff --git a/typings/index.d.ts b/typings/index.d.ts index 1cd568f4b..832bfae42 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2627,7 +2627,7 @@ declare module 'discord.js' { highWaterMark?: number; } - type SpeakingString = 'SPEAKING' | 'SOUNDSHARE'; + type SpeakingString = 'SPEAKING' | 'SOUNDSHARE' | 'PRIORITY_SPEAKING'; type StreamType = 'unknown' | 'converted' | 'opus' | 'ogg/opus' | 'webm/opus'; From 59205a21522a0052128270e5bc1106f6806b8cef Mon Sep 17 00:00:00 2001 From: Souji Date: Mon, 13 Jan 2020 15:02:31 +0100 Subject: [PATCH 1250/1359] fix: provide count on bulk deletion (#3682) * GuildAuditLogsEntry should provide count as extra in case of MESSAGE_BULK_DELETE * inner class: GuildAuditLogsEntry in GuildAuditLogs.js --- src/structures/GuildAuditLogs.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 53494fa45..d1185bdd9 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -323,6 +323,10 @@ class GuildAuditLogsEntry { count: data.options.count, channel: guild.channels.get(data.options.channel_id), }; + } else if (data.action_type === Actions.MESSAGE_BULK_DELETE) { + this.extra = { + count: data.options.count, + }; } else { switch (data.options.type) { case 'member': From 11f9118551b511b5c133eacb2550729e5b696434 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 15:07:54 +0100 Subject: [PATCH 1251/1359] fix(BitField): remove for..in in favor of Object.entries (#3650) * fix(BitField): remove for..in in favor of Object.keys * refactor: do not re-resolve bits Co-Authored-By: bdistin Co-authored-by: bdistin --- src/util/BitField.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/BitField.js b/src/util/BitField.js index f849e7b8a..8de151d01 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -103,7 +103,7 @@ class BitField { */ serialize(...hasParams) { const serialized = {}; - for (const perm in this.constructor.FLAGS) serialized[perm] = this.has(perm, ...hasParams); + for (const [flag, bit] of Object.entries(this.constructor.FLAGS)) serialized[flag] = this.has(bit, ...hasParams); return serialized; } From 400cb563580b6383019325069d40a2be529f4d07 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 15:12:18 +0100 Subject: [PATCH 1252/1359] fix(ShardingManager): assert shardList to be spawned, not totalShards (#3649) --- src/sharding/ShardingManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index f0298054c..7126148d0 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -235,7 +235,7 @@ class ShardingManager extends EventEmitter { */ fetchClientValues(prop) { if (this.shards.size === 0) return Promise.reject(new Error('SHARDING_NO_SHARDS')); - if (this.shards.size !== this.totalShards) return Promise.reject(new Error('SHARDING_IN_PROCESS')); + if (this.shards.size !== this.shardList.length) return Promise.reject(new Error('SHARDING_IN_PROCESS')); const promises = []; for (const shard of this.shards.values()) promises.push(shard.fetchClientValue(prop)); return Promise.all(promises); From 8014ddcd1c651749fc64e8dfc0b0863422d57203 Mon Sep 17 00:00:00 2001 From: Tenpi <37512637+Tenpi@users.noreply.github.com> Date: Mon, 13 Jan 2020 09:32:29 -0500 Subject: [PATCH 1253/1359] feat: dynamic property for ImageURLOptions (#3530) * Added dynamic property to ImageURLOptions * fixes * order * typings fix * made dynamic false by default * add curly spaces --- src/structures/Guild.js | 4 ++-- src/structures/User.js | 4 ++-- src/util/Constants.js | 12 +++++++----- typings/index.d.ts | 22 +++++++++++----------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 987fa38f2..6b22cd25b 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -433,9 +433,9 @@ class Guild extends Base { * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - iconURL({ format, size } = {}) { + iconURL({ format, size, dynamic } = {}) { if (!this.icon) return null; - return this.client.rest.cdn.Icon(this.id, this.icon, format, size); + return this.client.rest.cdn.Icon(this.id, this.icon, format, size, dynamic); } /** diff --git a/src/structures/User.js b/src/structures/User.js index a4b1d3d92..7ae67451b 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -140,9 +140,9 @@ class User extends Base { * @param {ImageURLOptions} [options={}] Options for the Image URL * @returns {?string} */ - avatarURL({ format, size } = {}) { + avatarURL({ format, size, dynamic } = {}) { if (!this.avatar) return null; - return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size); + return this.client.rest.cdn.Avatar(this.id, this.avatar, format, size, dynamic); } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 6ef681272..a2e86d4ae 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -119,7 +119,9 @@ function makeImageUrl(root, { format = 'webp', size } = {}) { * Options for Image URLs. * @typedef {Object} ImageURLOptions * @property {string} [format] One of `webp`, `png`, `jpg`, `gif`. If no format is provided, - * it will be `gif` for animated avatars or otherwise `webp` + * defaults to `webp`. + * @property {boolean} [dynamic] If true, the format will dynamically change to `gif` for + * animated avatars; the default is false. * @property {number} [size] One of `16`, `32`, `64`, `128`, `256`, `512`, `1024`, `2048` */ @@ -129,14 +131,14 @@ exports.Endpoints = { Emoji: (emojiID, format = 'png') => `${root}/emojis/${emojiID}.${format}`, Asset: name => `${root}/assets/${name}`, DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`, - Avatar: (userID, hash, format = 'default', size) => { - if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; + Avatar: (userID, hash, format = 'webp', size, dynamic = false) => { + if (dynamic) format = hash.startsWith('a_') ? 'gif' : format; return makeImageUrl(`${root}/avatars/${userID}/${hash}`, { format, size }); }, Banner: (guildID, hash, format = 'webp', size) => makeImageUrl(`${root}/banners/${guildID}/${hash}`, { format, size }), - Icon: (guildID, hash, format = 'default', size) => { - if (format === 'default') format = hash.startsWith('a_') ? 'gif' : 'webp'; + Icon: (guildID, hash, format = 'webp', size, dynamic = false) => { + if (dynamic) format = hash.startsWith('a_') ? 'gif' : format; return makeImageUrl(`${root}/icons/${guildID}/${hash}`, { format, size }); }, AppIcon: (clientID, hash, { format = 'webp', size } = {}) => diff --git a/typings/index.d.ts b/typings/index.d.ts index 832bfae42..c5eb84f91 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -272,9 +272,9 @@ declare module 'discord.js' { public name: string; public owner: User | Team | null; public rpcOrigins: string[]; - public coverImage(options?: AvatarOptions): string; + public coverImage(options?: ImageURLOptions): string; public fetchAssets(): Promise; - public iconURL(options?: AvatarOptions): string; + public iconURL(options?: ImageURLOptions): string; public toJSON(): object; public toString(): string; } @@ -291,7 +291,7 @@ declare module 'discord.js' { public readonly createdAt: Date; public readonly createdTimestamp: number; - public iconURL(options?: AvatarOptions): string; + public iconURL(options?: ImageURLOptions): string; public toJSON(): object; public toString(): string; } @@ -726,7 +726,7 @@ declare module 'discord.js' { public widgetChannelID: Snowflake | null; public widgetEnabled: boolean | null; public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; - public bannerURL(options?: AvatarOptions): string | null; + public bannerURL(options?: ImageURLOptions): string | null; public createIntegration(data: IntegrationData, reason?: string): Promise; public delete(): Promise; public edit(data: GuildEditData, reason?: string): Promise; @@ -740,7 +740,7 @@ declare module 'discord.js' { public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; - public iconURL(options?: AvatarOptions): string | null; + public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; public leave(): Promise; public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; @@ -759,7 +759,7 @@ declare module 'discord.js' { public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise; public setVerificationLevel(verificationLevel: number, reason?: string): Promise; - public splashURL(options?: AvatarOptions): string | null; + public splashURL(options?: ImageURLOptions): string | null; public toJSON(): object; public toString(): string; } @@ -1189,8 +1189,8 @@ declare module 'discord.js' { public largeText: string | null; public smallImage: Snowflake | null; public smallText: string | null; - public largeImageURL(options: AvatarOptions): string | null; - public smallImageURL(options: AvatarOptions): string | null; + public largeImageURL(options: ImageURLOptions): string | null; + public smallImageURL(options: ImageURLOptions): string | null; } export class Role extends Base { @@ -1421,10 +1421,10 @@ declare module 'discord.js' { public system?: boolean; public readonly tag: string; public username: string; - public avatarURL(options?: AvatarOptions): string | null; + public avatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; public createDM(): Promise; public deleteDM(): Promise; - public displayAvatarURL(options?: AvatarOptions): string; + public displayAvatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string; public equals(user: User): boolean; public fetch(): Promise; public toString(): string; @@ -2011,7 +2011,7 @@ declare module 'discord.js' { new?: any; } - interface AvatarOptions { + interface ImageURLOptions { format?: ImageExt; size?: ImageSize; } From 62afafdbe9674801d8f8c52c0fe14c3d1bc90adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Mon, 13 Jan 2020 16:48:49 +0100 Subject: [PATCH 1254/1359] typings: Fixed build error (#3689) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c5eb84f91..c217cdcbf 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1635,7 +1635,7 @@ declare module 'discord.js' { export class Webhook extends WebhookMixin() { constructor(client: Client, data?: object); public avatar: string; - public avatarURL(options?: AvatarOptions): string | null; + public avatarURL(options?: ImageURLOptions): string | null; public channelID: Snowflake; public guildID: Snowflake; public name: string; From 53a1f8fcd4e333e60c32f9f51dc4062708eec207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Rom=C3=A1n?= Date: Mon, 13 Jan 2020 17:16:22 +0100 Subject: [PATCH 1255/1359] refactor: Remove `util` alias export (#3691) --- src/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.js b/src/index.js index cbc0d5a79..896919c40 100644 --- a/src/index.js +++ b/src/index.js @@ -28,7 +28,6 @@ module.exports = { Structures: require('./util/Structures'), SystemChannelFlags: require('./util/SystemChannelFlags'), Util: Util, - util: Util, version: require('../package.json').version, // Stores From 45cd58b68ce61afcf44793e610f06ada44fb8d1f Mon Sep 17 00:00:00 2001 From: Ayyan Lewis Date: Mon, 13 Jan 2020 21:01:16 +0400 Subject: [PATCH 1256/1359] types(VoiceBroadcast): add subscribers property (#3677) * types(VoiceBroadcast): add subscribers property * types(VoiceBroadcast): change player property to private Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- typings/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index c217cdcbf..3d55cb97e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1475,7 +1475,8 @@ declare module 'discord.js' { class VoiceBroadcast extends EventEmitter { constructor(client: Client); public client: Client; - public dispatchers: StreamDispatcher[]; + public subscribers: StreamDispatcher[]; + private player: BroadcastAudioPlayer; public readonly dispatcher: BroadcastDispatcher; public play(input: string | Readable, options?: StreamOptions): BroadcastDispatcher; From c23cc7a42ef7a301b04fde19e58782a903b6d301 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 13 Jan 2020 20:53:07 +0200 Subject: [PATCH 1257/1359] src: Cleanup event listeners on WebSocket connections (#3681) * src: Cleanup event listeners on WebSocket connections Should prevent #3641 from happening, as well as double connections on a shard * typings: Forgot to add the method --- src/client/websocket/WebSocketManager.js | 2 ++ src/client/websocket/WebSocketShard.js | 31 ++++++++++++++++++++---- typings/index.d.ts | 1 + 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 71cbf93c0..becc570c8 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -245,6 +245,8 @@ class WebSocketManager extends EventEmitter { }); shard.on(ShardEvents.DESTROYED, () => { + shard._cleanupConnection(); + this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); this.client.emit(Events.SHARD_RECONNECTING, shard.id); diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 5171d7e59..1b7d5495b 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -208,11 +208,18 @@ class WebSocketShard extends EventEmitter { this.once(ShardEvents.INVALID_SESSION, onInvalid); if (this.connection && this.connection.readyState === WebSocket.OPEN) { - this.debug('Connection found, attempting an immediate identify.'); + this.debug('An open connection was found, attempting an immediate identify.'); this.identify(); return; } + if (this.connection) { + this.debug(`A connection was found. Cleaning up before continuing. + State: ${CONNECTION_STATE[this.connection.readyState]}`); + this._cleanupConnection(); + this.connection.close(1000); + } + const wsQuery = { v: client.options.ws.version }; if (zlib) { @@ -526,9 +533,9 @@ class WebSocketShard extends EventEmitter { } else if (!this.lastHeartbeatAcked) { this.debug( `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. - Status : ${STATUS_KEYS[this.status]} - Sequence : ${this.sequence} - Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` + Status : ${STATUS_KEYS[this.status]} + Sequence : ${this.sequence} + Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` ); this.destroy(4009); return; @@ -629,7 +636,8 @@ class WebSocketShard extends EventEmitter { */ _send(data) { if (!this.connection || this.connection.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available!`); + this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available! Resetting the shard...`); + this.destroy(4000); return; } @@ -667,6 +675,8 @@ class WebSocketShard extends EventEmitter { * @private */ destroy(closeCode = 1000, cleanup = false) { + this.debug(`Destroying with close code ${closeCode}, attempting a reconnect: ${!cleanup}`); + this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); @@ -696,6 +706,17 @@ class WebSocketShard extends EventEmitter { this.ratelimit.timer = null; } } + + /** + * Cleans up the WebSocket connection listeners. + * @private + */ + _cleanupConnection() { + this.connection.onopen = + this.connection.onclose = + this.connection.onerror = + this.connection.onmessage = null; + } } module.exports = WebSocketShard; diff --git a/typings/index.d.ts b/typings/index.d.ts index 3d55cb97e..d4160b59c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1717,6 +1717,7 @@ declare module 'discord.js' { private _send(data: object): void; private processQueue(): void; private destroy(closeCode: number): void; + private _cleanupConnection(): void; public send(data: object): void; public on(event: 'ready', listener: () => void): this; From ee0b7c155a1767ed42e2756e53d56368e8b69929 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 13 Jan 2020 21:28:29 +0100 Subject: [PATCH 1258/1359] feat(Presence): add support for multiple activities (#3661) * feat(Presence): add support for multiple activites * typings(Presence): fix spelling of 'activities' Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- src/structures/Presence.js | 7 +++---- typings/index.d.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 8c207989f..1ad6229a4 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -85,12 +85,11 @@ class Presence { */ this.status = data.status || this.status || 'offline'; - const activity = data.game || data.activity; /** - * The activity of this presence - * @type {?Activity} + * The activities of this presence + * @type {Activity[]} */ - this.activity = activity ? new Activity(this, activity) : null; + this.activities = data.activities ? data.activities.map(activity => new Activity(this, activity)) : []; /** * The devices this presence is on diff --git a/typings/index.d.ts b/typings/index.d.ts index d4160b59c..9e64598b0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1140,7 +1140,7 @@ declare module 'discord.js' { export class Presence { constructor(client: Client, data?: object); - public activity: Activity | null; + public activities: Activity[]; public clientStatus: ClientPresenceStatusData | null; public flags: Readonly; public guild: Guild | null; From 629c57f890fc5124384ae83f74c9190c1db2dd4c Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Mon, 13 Jan 2020 22:29:05 +0000 Subject: [PATCH 1259/1359] fix: regression (changing voice servers) --- src/client/voice/VoiceConnection.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 540dd8c74..b4b1e9da1 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -452,8 +452,7 @@ class VoiceConnection extends EventEmitter { onSessionDescription(data) { Object.assign(this.authentication, data); this.status = VoiceStatus.CONNECTED; - const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); - dispatcher.on('finish', () => { + const ready = () => { this.client.clearTimeout(this.connectTimeout); this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`); /** @@ -462,7 +461,14 @@ class VoiceConnection extends EventEmitter { * @event VoiceConnection#ready */ this.emit('ready'); - }); + }; + if (this.dispatcher) { + ready(); + } else { + // This serves to provide support for voice receive, sending audio is required to receive it. + const dispatcher = this.play(new SingleSilence(), { type: 'opus' }); + dispatcher.once('finish', ready); + } } onStartSpeaking({ user_id, ssrc, speaking }) { From 75fe1faf2f95775ae29883bfcf6f27651a1b53b2 Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Mon, 13 Jan 2020 17:45:58 -0500 Subject: [PATCH 1260/1359] Remove BroadcastAudioPlayer from typings (#3692) --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 9e64598b0..3e6aec40f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1476,7 +1476,6 @@ declare module 'discord.js' { constructor(client: Client); public client: Client; public subscribers: StreamDispatcher[]; - private player: BroadcastAudioPlayer; public readonly dispatcher: BroadcastDispatcher; public play(input: string | Readable, options?: StreamOptions): BroadcastDispatcher; From 7f99be739aa7f11b6323a08d9435acb784aeb011 Mon Sep 17 00:00:00 2001 From: Souji Date: Tue, 14 Jan 2020 11:28:19 +0100 Subject: [PATCH 1261/1359] docs(MessageMentions): add sort order notice (#3693) * mention order returned from API * not left to right in text --- src/structures/MessageMentions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 29a029bf7..81bd791c3 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -42,6 +42,7 @@ class MessageMentions { if (users instanceof Collection) { /** * Any users that were mentioned + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} */ this.users = new Collection(users); @@ -63,6 +64,7 @@ class MessageMentions { if (roles instanceof Collection) { /** * Any roles that were mentioned + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} */ this.roles = new Collection(roles); @@ -104,6 +106,7 @@ class MessageMentions { if (crosspostedChannels instanceof Collection) { /** * A collection of crossposted channels + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} */ this.crosspostedChannels = new Collection(crosspostedChannels); @@ -127,6 +130,7 @@ class MessageMentions { /** * Any members that were mentioned (only in {@link TextChannel}s) + * Order as received from the API, not left to right by occurence in the message content * @type {?Collection} * @readonly */ @@ -143,6 +147,7 @@ class MessageMentions { /** * Any channels that were mentioned + * Order as received from the API, not left to right by occurence in the message content * @type {Collection} * @readonly */ From d77229f423a2f2263a09a6af9b670d0ee48752ef Mon Sep 17 00:00:00 2001 From: Helmasaur Date: Thu, 16 Jan 2020 12:57:20 +0100 Subject: [PATCH 1262/1359] chore: ffmpeg package in the voice doc (#3697) ffmpeg package changed from "ffmpeg-binaries" to "ffmpeg-static" --- docs/topics/voice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index 22f5c93de..cf336eb57 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -4,7 +4,7 @@ Voice in discord.js can be used for many things, such as music bots, recording o In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a `VoiceConnection`, where you can start streaming and receiving audio. To get started, make sure you have: -* FFmpeg - `npm install ffmpeg-binaries` +* FFmpeg - `npm install ffmpeg-static` * an opus encoder, choose one from below: * `npm install node-opus` (better performance) * `npm install opusscript` From d096e40f6fe025001dff64f0bee37ac26962998e Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 16 Jan 2020 12:59:03 +0100 Subject: [PATCH 1263/1359] feat/fix: use updated eslint action (#3699) --- .github/workflows/test-cron.yml | 2 +- .github/workflows/test.yml | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-cron.yml b/.github/workflows/test-cron.yml index 99ee10670..10f97e38f 100644 --- a/.github/workflows/test-cron.yml +++ b/.github/workflows/test-cron.yml @@ -19,7 +19,7 @@ jobs: run: npm install - name: Run ESLint - run: npm run lint + uses: icrawl/action-eslint@v1 typings: name: TSLint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 641a6327b..0c51d68e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,11 +17,7 @@ jobs: run: npm install - name: Run ESLint - uses: discordjs/action-eslint@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - job-name: ESLint + uses: icrawl/action-eslint@v1 typings: name: TSLint From 6a0fe467e5f6e08360bcce0bf3039f01e6dc8678 Mon Sep 17 00:00:00 2001 From: Crawl Date: Thu, 16 Jan 2020 14:10:48 +0100 Subject: [PATCH 1264/1359] docs: replace all occurances of node-opus with @discordjs/opus (#3698) * docs: replace all occurances of node-opus with @discordjs/opus * chore: leave in node-opus in case not everyone switched --- .github/CONTRIBUTING.md | 2 +- README.md | 8 ++++---- docs/general/faq.md | 10 +++++----- docs/general/welcome.md | 8 ++++---- docs/topics/voice.md | 8 ++++---- package.json | 1 + 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9f84b1073..fd6de7242 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,7 +11,7 @@ To get ready to work on the codebase, please do the following: 1. Fork & clone the repository, and make sure you're on the **master** branch 2. Run `npm install` -3. If you're working on voice, also run `npm install node-opus` or `npm install opusscript` +3. If you're working on voice, also run `npm install @discordjs/opus` or `npm install opusscript` 4. Code your heart out! 5. Run `npm test` to run ESLint and ensure any JSDoc changes are valid 6. [Submit a pull request](https://github.com/discordjs/discord.js/compare) diff --git a/README.md b/README.md index 2003f60d5..3ec09092d 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,13 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discordjs/discord.js @discordjs/opus` With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines -The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. -Using opusscript is only recommended for development environments where node-opus is tough to get working. -For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. +The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus. +Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working. +For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) diff --git a/docs/general/faq.md b/docs/general/faq.md index c4ce363d8..d57060dba 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -7,8 +7,8 @@ Update to Node.js 10.0.0 or newer. ## How do I get voice working? - Install FFMPEG. -- Install either the `node-opus` package or the `opusscript` package. - node-opus is greatly preferred, due to it having significantly better performance. +- Install either the `@discordjs/opus` package or the `opusscript` package. + @discordjs/opus is greatly preferred, due to it having significantly better performance. ## How do I install FFMPEG? - **npm:** `npm install ffmpeg-binaries` @@ -16,10 +16,10 @@ Update to Node.js 10.0.0 or newer. - **Ubuntu 14.04:** `sudo apt-get install libav-tools` - **Windows:** `npm install ffmpeg-binaries` or see the [FFMPEG section of AoDude's guide](https://github.com/bdistin/OhGodMusicBot/blob/master/README.md#download-ffmpeg). -## How do I set up node-opus? -- **Ubuntu:** Simply run `npm install node-opus`, and it's done. Congrats! +## How do I set up @discordjs/opus? +- **Ubuntu:** Simply run `npm install @discordjs/opus`, and it's done. Congrats! - **Windows:** Run `npm install --global --production windows-build-tools` in an admin command prompt or PowerShell. - Then, running `npm install node-opus` in your bot's directory should successfully build it. Woo! + Then, running `npm install @discordjs/opus` in your bot's directory should successfully build it. Woo! Other questions can be found at the [official Discord.js guide](https://discordjs.guide/popular-topics/common-questions.html) If you have issues not listed here or on the guide, feel free to ask in the [official Discord.js server](https://discord.gg/bRCvFy9). diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 52cb3135f..936c01a48 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -37,13 +37,13 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` -With voice support ([node-opus](https://www.npmjs.com/package/node-opus)): `npm install discordjs/discord.js node-opus` +With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discordjs/discord.js @discordjs/opus` With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discordjs/discord.js opusscript` ### Audio engines -The preferred audio engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. -Using opusscript is only recommended for development environments where node-opus is tough to get working. -For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. +The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus. +Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working. +For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) diff --git a/docs/topics/voice.md b/docs/topics/voice.md index cf336eb57..a716d3e7b 100644 --- a/docs/topics/voice.md +++ b/docs/topics/voice.md @@ -6,13 +6,13 @@ In discord.js, you can use voice by connecting to a `VoiceChannel` to obtain a ` To get started, make sure you have: * FFmpeg - `npm install ffmpeg-static` * an opus encoder, choose one from below: - * `npm install node-opus` (better performance) + * `npm install @discordjs/opus` (better performance) * `npm install opusscript` * a good network connection -The preferred opus engine is node-opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose node-opus. -Using opusscript is only recommended for development environments where node-opus is tough to get working. -For production bots, using node-opus should be considered a necessity, especially if they're going to be running on multiple servers. +The preferred opus engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus. +Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working. +For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ## Joining a voice channel The example below reacts to a message and joins the sender's voice channel, catching any errors. This is important diff --git a/package.json b/package.json index a52ade25f..230ec4709 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "node": ">=10.2.0" }, "browser": { + "@discordjs/opus": false, "https": false, "ws": false, "erlpack": false, From 69c79a4136619e4dcacfffb935aa9bd60aa2081c Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Fri, 17 Jan 2020 14:11:14 -0500 Subject: [PATCH 1265/1359] typings/docs(GuildEmoji): id isn't nullable (#3694) * Fix: GuildEmoji#id isn't nullable * Move ID to be alphabetical * Add JSDoc to say it's not nullable * fix linting --- src/structures/GuildEmoji.js | 6 ++++++ typings/index.d.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index 79fff52f7..a7620911d 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -24,6 +24,12 @@ class GuildEmoji extends Emoji { */ this.guild = guild; + /** + * The ID of this emoji + * @type {Snowflake} + * @name GuildEmoji#id + */ + this._roles = []; this._patch(data); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 3e6aec40f..d567e6db6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -841,6 +841,7 @@ declare module 'discord.js' { public available: boolean; public readonly deletable: boolean; public guild: Guild; + public id: Snowflake; public managed: boolean; public requiresColons: boolean; public roles: GuildEmojiRoleStore; From cbb8db3058464cd99186da294ad781af2a7f317e Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Sun, 19 Jan 2020 05:24:55 -0500 Subject: [PATCH 1266/1359] feat(Collectors): make collectors auto-stop when relevant structures are deleted (#3632) * Collectors: make Collectors automatically stop when Channel, Guild, or Message are deleted. * fix potential error with DM collectors * Message collectors dont have a `this.message` you dummy * Fix(various): nitpicks, documentation, typings, and stray error * Pleasing mr tslint * fix: typings * Grammatical fixes Co-Authored-By: SpaceEEC * Fixing the linting after space's suggestions * docs(ReactionCollector): remove whitespace Co-authored-by: SpaceEEC --- src/structures/MessageCollector.js | 31 +++++++++++++++++++ src/structures/ReactionCollector.js | 47 +++++++++++++++++++++++++++++ typings/index.d.ts | 7 +++++ 3 files changed, 85 insertions(+) diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index 5d45be089..f357722b5 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -11,6 +11,7 @@ const { Events } = require('../util/Constants'); /** * Collects messages on a channel. + * Will automatically stop if the channel (`'channelDelete'`) or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ class MessageCollector extends Collector { @@ -38,16 +39,22 @@ class MessageCollector extends Collector { const bulkDeleteListener = (messages => { for (const message of messages.values()) this.handleDispose(message); }).bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_CREATE, this.handleCollect); this.client.on(Events.MESSAGE_DELETE, this.handleDispose); this.client.on(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); this.once('end', () => { this.client.removeListener(Events.MESSAGE_CREATE, this.handleCollect); this.client.removeListener(Events.MESSAGE_DELETE, this.handleDispose); this.client.removeListener(Events.MESSAGE_BULK_DELETE, bulkDeleteListener); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); } @@ -93,6 +100,30 @@ class MessageCollector extends Collector { if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; return null; } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.channel.guild && guild.id === this.channel.guild.id) { + this.stop('guildDelete'); + } + } } module.exports = MessageCollector; diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index 8e84fb672..e7d5aeb16 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -13,6 +13,8 @@ const { Events } = require('../util/Constants'); /** * Collects reactions on messages. + * Will automatically stop if the message (`'messageDelete'`), + * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ class ReactionCollector extends Collector { @@ -43,16 +45,25 @@ class ReactionCollector extends Collector { this.total = 0; this.empty = this.empty.bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + this._handleMessageDeletion = this._handleMessageDeletion.bind(this); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() + 1); this.client.on(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.on(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.on(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); this.once('end', () => { this.client.removeListener(Events.MESSAGE_REACTION_ADD, this.handleCollect); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE, this.handleDispose); this.client.removeListener(Events.MESSAGE_REACTION_REMOVE_ALL, this.empty); + this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion); + this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); + this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); if (this.client.getMaxListeners() !== 0) this.client.setMaxListeners(this.client.getMaxListeners() - 1); }); @@ -131,6 +142,42 @@ class ReactionCollector extends Collector { return null; } + /** + * Handles checking if the message has been deleted, and if so, stops the collector with the reason 'messageDelete'. + * @private + * @param {Message} message The message that was deleted + * @returns {void} + */ + _handleMessageDeletion(message) { + if (message.id === this.message.id) { + this.stop('messageDelete'); + } + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.message.channel.id) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (this.message.guild && guild.id === this.message.guild.id) { + this.stop('guildDelete'); + } + } + /** * Gets the collector key for a reaction. * @param {MessageReaction} reaction The message reaction to get the key for diff --git a/typings/index.d.ts b/typings/index.d.ts index d567e6db6..6d719a347 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1023,6 +1023,9 @@ declare module 'discord.js' { export class MessageCollector extends Collector { constructor(channel: TextChannel | DMChannel, filter: CollectorFilter, options?: MessageCollectorOptions); + private _handleChannelDeletion(channel: GuildChannel): void; + private _handleGuildDeletion(guild: Guild): void; + public channel: Channel; public options: MessageCollectorOptions; public received: number; @@ -1153,6 +1156,10 @@ declare module 'discord.js' { export class ReactionCollector extends Collector { constructor(message: Message, filter: CollectorFilter, options?: ReactionCollectorOptions); + private _handleChannelDeletion(channel: GuildChannel): void; + private _handleGuildDeletion(guild: Guild): void; + private _handleMessageDeletion(message: Message): void; + public message: Message; public options: ReactionCollectorOptions; public total: number; From f501d06c0d5de872327be683c7f80d962eff3864 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 19 Jan 2020 13:05:45 +0100 Subject: [PATCH 1267/1359] fix(Presence): account for multiple activities everywhere (#3703) * fix(Presence): account for multiple activities everywhere * refactor(Presence): make initialization of 'activities' more readable --- src/structures/ClientPresence.js | 2 +- src/structures/ClientUser.js | 2 +- src/structures/Presence.js | 27 +++++++++++++++++---------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 90db53bd0..213b46f2d 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -68,7 +68,7 @@ class ClientPresence extends Presence { }; if ((status || afk || since) && !activity) { - packet.game = this.activity; + packet.game = this.activities[0] || null; } if (packet.game) { diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 16dd13d07..3f2883efc 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -149,7 +149,7 @@ class ClientUser extends Structures.get('User') { * @example * // Set the client user's activity * client.user.setActivity('discord.js', { type: 'WATCHING' }) - * .then(presence => console.log(`Activity set to ${presence.activity.name}`)) + * .then(presence => console.log(`Activity set to ${presence.activities[0].name}`)) * .catch(console.error); */ setActivity(name, options = {}) { diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 1ad6229a4..12888f1c6 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -85,11 +85,17 @@ class Presence { */ this.status = data.status || this.status || 'offline'; - /** - * The activities of this presence - * @type {Activity[]} - */ - this.activities = data.activities ? data.activities.map(activity => new Activity(this, activity)) : []; + if (data.activities) { + /** + * The activities of this presence + * @type {Activity[]} + */ + this.activities = data.activities.map(activity => new Activity(this, activity)); + } else if (data.activity || data.game) { + this.activities = [new Activity(this, data.game || data.activity)]; + } else { + this.activities = []; + } /** * The devices this presence is on @@ -105,7 +111,7 @@ class Presence { _clone() { const clone = Object.assign(Object.create(this), this); - if (this.activity) clone.activity = this.activity._clone(); + if (this.activities) clone.activities = this.activities.map(activity => activity._clone()); return clone; } @@ -118,10 +124,11 @@ class Presence { return this === presence || ( presence && this.status === presence.status && - this.activity ? this.activity.equals(presence.activity) : !presence.activity && - this.clientStatus.web === presence.clientStatus.web && - this.clientStatus.mobile === presence.clientStatus.mobile && - this.clientStatus.desktop === presence.clientStatus.desktop + this.activities.length === presence.activities.length && + this.activities.every((activity, index) => activity.equals(presence.activities[index])) && + this.clientStatus.web === presence.clientStatus.web && + this.clientStatus.mobile === presence.clientStatus.mobile && + this.clientStatus.desktop === presence.clientStatus.desktop ); } From 6302afb84bb05907d12790280c6074ff850497a3 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 19 Jan 2020 13:06:21 +0100 Subject: [PATCH 1268/1359] docs(MessageMentions): channels are actually in order (#3705) * docs(MessageMentions): channels are actually in order * docs(MessageMentions): readd info about order for channels * docs(MessageMentions): reword info to account for rtl locales --- src/structures/MessageMentions.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 81bd791c3..ab2d5c81b 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -42,7 +42,7 @@ class MessageMentions { if (users instanceof Collection) { /** * Any users that were mentioned - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {Collection} */ this.users = new Collection(users); @@ -64,7 +64,7 @@ class MessageMentions { if (roles instanceof Collection) { /** * Any roles that were mentioned - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {Collection} */ this.roles = new Collection(roles); @@ -106,7 +106,7 @@ class MessageMentions { if (crosspostedChannels instanceof Collection) { /** * A collection of crossposted channels - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {Collection} */ this.crosspostedChannels = new Collection(crosspostedChannels); @@ -130,7 +130,7 @@ class MessageMentions { /** * Any members that were mentioned (only in {@link TextChannel}s) - * Order as received from the API, not left to right by occurence in the message content + * Order as received from the API, not as they appear in the message content * @type {?Collection} * @readonly */ @@ -147,7 +147,7 @@ class MessageMentions { /** * Any channels that were mentioned - * Order as received from the API, not left to right by occurence in the message content + * Order as they appear first in the message content * @type {Collection} * @readonly */ From 877577badcaa17b0221b75d79c55b6de3eff4e3a Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 20 Jan 2020 21:02:02 +0000 Subject: [PATCH 1269/1359] typings(RichPresenceAssets): *ImageURL's options are optional (#3727) --- typings/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6d719a347..457f86a62 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1197,8 +1197,8 @@ declare module 'discord.js' { public largeText: string | null; public smallImage: Snowflake | null; public smallText: string | null; - public largeImageURL(options: ImageURLOptions): string | null; - public smallImageURL(options: ImageURLOptions): string | null; + public largeImageURL(options?: ImageURLOptions): string | null; + public smallImageURL(options?: ImageURLOptions): string | null; } export class Role extends Base { From 63293fe14d99a2915adc52681dc5a722ea81f464 Mon Sep 17 00:00:00 2001 From: Carter <45381083+Fyko@users.noreply.github.com> Date: Wed, 22 Jan 2020 01:21:43 -0700 Subject: [PATCH 1270/1359] chore(License): bump license year (#3734) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9a4257e6c..9997d13f9 100644 --- a/LICENSE +++ b/LICENSE @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright 2015 - 2019 Amish Shah + Copyright 2015 - 2020 Amish Shah Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From c779fe3670b2279de8638de6f2e70e008b1b1804 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 24 Jan 2020 14:29:53 +0000 Subject: [PATCH 1271/1359] feat(Guild): add fetchBan method (#3726) * Add error for not resolving ID to fetch ban * Add Guild#fetchBan * add missing ! * typings * lint fixes * add jsdoc description --- src/errors/Messages.js | 1 + src/structures/Guild.js | 15 +++++++++++++++ typings/index.d.ts | 1 + 3 files changed, 17 insertions(+) diff --git a/src/errors/Messages.js b/src/errors/Messages.js index ae42f73c5..db339ca2c 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -71,6 +71,7 @@ const Messages = { SPLIT_MAX_LEN: 'Chunk exceeds the max length and contains no split characters.', BAN_RESOLVE_ID: (ban = false) => `Couldn't resolve the user ID to ${ban ? 'ban' : 'unban'}.`, + FETCH_BAN_RESOLVE_ID: 'Couldn\'t resolve the user ID to fetch the ban.', PRUNE_DAYS_TYPE: 'Days must be a number', diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 6b22cd25b..3dd518d40 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -554,6 +554,21 @@ class Guild extends Base { * @property {?string} reason Reason the user was banned */ + /** + * Fetches information on a banned user from this guild. + * @param {UserResolvable} user The User to fetch the ban info of + * @returns {BanInfo} + */ + fetchBan(user) { + const id = this.client.users.resolveID(user); + if (!id) throw new Error('FETCH_BAN_RESOLVE_ID'); + return this.client.api.guilds(this.id).bans(id).get() + .then(ban => ({ + reason: ban.reason, + user: this.client.users.add(ban.user), + })); + } + /** * Fetches a collection of banned users in this guild. * @returns {Promise>} diff --git a/typings/index.d.ts b/typings/index.d.ts index 457f86a62..4c5a5ae28 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -733,6 +733,7 @@ declare module 'discord.js' { public equals(guild: Guild): boolean; public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; + public fetchBan(user: UserResolvable): Promise<{ user: User, reason: string }>; public fetchBans(): Promise>; public fetchEmbed(): Promise; public fetchIntegrations(): Promise>; From 929ff9ccd0f1aa3c1d070915d567f594077b8288 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Fri, 24 Jan 2020 10:38:26 -0500 Subject: [PATCH 1272/1359] feat(Client): add support for INVITE_CREATE and INVITE_DELETE events (#3720) * Add support for new Invite events * Merge typings for events Co-Authored-By: Sugden * Add warning about requiring permissions * Null check channel and guild * fix: .guilds not .channels --- src/client/actions/ActionsManager.js | 2 ++ src/client/actions/InviteCreate.js | 28 ++++++++++++++++++ src/client/actions/InviteDelete.js | 29 +++++++++++++++++++ .../websocket/handlers/INVITE_CREATE.js | 5 ++++ .../websocket/handlers/INVITE_DELETE.js | 5 ++++ src/util/Constants.js | 4 +++ typings/index.d.ts | 1 + 7 files changed, 74 insertions(+) create mode 100644 src/client/actions/InviteCreate.js create mode 100644 src/client/actions/InviteDelete.js create mode 100644 src/client/websocket/handlers/INVITE_CREATE.js create mode 100644 src/client/websocket/handlers/INVITE_DELETE.js diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 268e4371b..730f507c0 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -16,6 +16,8 @@ class ActionsManager { this.register(require('./ChannelUpdate')); this.register(require('./GuildDelete')); this.register(require('./GuildUpdate')); + this.register(require('./InviteCreate')); + this.register(require('./InviteDelete')); this.register(require('./GuildMemberRemove')); this.register(require('./GuildBanRemove')); this.register(require('./GuildRoleCreate')); diff --git a/src/client/actions/InviteCreate.js b/src/client/actions/InviteCreate.js new file mode 100644 index 000000000..5552ea2f0 --- /dev/null +++ b/src/client/actions/InviteCreate.js @@ -0,0 +1,28 @@ +'use strict'; + +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteCreateAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.get(data.channel_id); + const guild = client.guilds.get(data.guild_id); + if (!channel && !guild) return false; + + const inviteData = Object.assign(data, { channel, guild }); + const invite = new Invite(client, inviteData); + /** + * Emitted when an invite is created. + * This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel. + * @event Client#inviteCreate + * @param {Invite} invite The invite that was created + */ + client.emit(Events.INVITE_CREATE, invite); + return { invite }; + } +} + +module.exports = InviteCreateAction; diff --git a/src/client/actions/InviteDelete.js b/src/client/actions/InviteDelete.js new file mode 100644 index 000000000..83933d34f --- /dev/null +++ b/src/client/actions/InviteDelete.js @@ -0,0 +1,29 @@ +'use strict'; + +const Action = require('./Action'); +const Invite = require('../../structures/Invite'); +const { Events } = require('../../util/Constants'); + +class InviteDeleteAction extends Action { + handle(data) { + const client = this.client; + const channel = client.channels.get(data.channel_id); + const guild = client.guilds.get(data.guild_id); + if (!channel && !guild) return false; + + const inviteData = Object.assign(data, { channel, guild }); + const invite = new Invite(client, inviteData); + + /** + * Emitted when an invite is deleted. + * This event only triggers if the client has `MANAGE_GUILD` permissions for the guild, + * or `MANAGE_CHANNEL` permissions for the channel. + * @event Client#inviteDelete + * @param {Invite} invite The invite that was deleted + */ + client.emit(Events.INVITE_DELETE, invite); + return { invite }; + } +} + +module.exports = InviteDeleteAction; diff --git a/src/client/websocket/handlers/INVITE_CREATE.js b/src/client/websocket/handlers/INVITE_CREATE.js new file mode 100644 index 000000000..50a2e72dd --- /dev/null +++ b/src/client/websocket/handlers/INVITE_CREATE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.InviteCreate.handle(packet.d); +}; diff --git a/src/client/websocket/handlers/INVITE_DELETE.js b/src/client/websocket/handlers/INVITE_DELETE.js new file mode 100644 index 000000000..597185234 --- /dev/null +++ b/src/client/websocket/handlers/INVITE_DELETE.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.InviteDelete.handle(packet.d); +}; diff --git a/src/util/Constants.js b/src/util/Constants.js index a2e86d4ae..a8f93a856 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -243,6 +243,8 @@ exports.Events = { GUILD_INTEGRATIONS_UPDATE: 'guildIntegrationsUpdate', GUILD_ROLE_CREATE: 'roleCreate', GUILD_ROLE_DELETE: 'roleDelete', + INVITE_CREATE: 'inviteCreate', + INVITE_DELETE: 'inviteDelete', GUILD_ROLE_UPDATE: 'roleUpdate', GUILD_EMOJI_CREATE: 'emojiCreate', GUILD_EMOJI_DELETE: 'emojiDelete', @@ -353,6 +355,8 @@ exports.WSEvents = keyMirror([ 'GUILD_CREATE', 'GUILD_DELETE', 'GUILD_UPDATE', + 'INVITE_CREATE', + 'INVITE_DELETE', 'GUILD_MEMBER_ADD', 'GUILD_MEMBER_REMOVE', 'GUILD_MEMBER_UPDATE', diff --git a/typings/index.d.ts b/typings/index.d.ts index 4c5a5ae28..fc23f027f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -188,6 +188,7 @@ declare module 'discord.js' { public on(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; + public on(event: 'inviteCreate' | 'inviteDelete', listener: (invite: Invite) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; From b81f771007739bc56dd82bad235dedeb67b3ff6e Mon Sep 17 00:00:00 2001 From: Gryffon Bellish Date: Fri, 24 Jan 2020 10:58:23 -0500 Subject: [PATCH 1273/1359] cleanup: fix deepscan issues (#3740) * fix: don't double check if shards are auto. * fix: remove useless roles array. * fix: remove useless undefined checks. * fix: remove useless `this` binding * Apply suggestions from code review Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com> * Fix: Space's suggestion * Fix: time is always truthy * Check if it's an invalid date. Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> --- src/client/voice/dispatcher/StreamDispatcher.js | 4 ++-- src/client/websocket/WebSocketManager.js | 2 +- src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- src/structures/Guild.js | 1 + src/structures/MessageCollector.js | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 2923eb61e..20a63fd9f 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -73,10 +73,10 @@ class StreamDispatcher extends Writable { this._setSpeaking(0); }); - if (typeof volume !== 'undefined') this.setVolume(volume); + this.setVolume(volume); + this.setBitrate(bitrate); if (typeof fec !== 'undefined') this.setFEC(fec); if (typeof plp !== 'undefined') this.setPLP(plp); - if (typeof bitrate !== 'undefined') this.setBitrate(bitrate); const streamError = (type, err) => { /** diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index becc570c8..1c22a5bfe 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -158,7 +158,7 @@ class WebSocketManager extends EventEmitter { if (shards === 'auto') { this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); this.totalShards = this.client.options.shardCount = recommendedShards; - if (shards === 'auto' || !this.client.options.shards.length) { + if (!this.client.options.shards.length) { this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); } } diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index da73693cb..6da643ff3 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -6,7 +6,7 @@ module.exports = (client, { d: data }) => { const channel = client.channels.get(data.channel_id); const time = new Date(data.last_pin_timestamp); - if (channel && time) { + if (channel && !Number.isNaN(time.getTime())) { // Discord sends null for last_pin_timestamp if the last pinned message was removed channel.lastPinTimestamp = time.getTime() || null; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 3dd518d40..5827ff49b 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -775,6 +775,7 @@ class Guild extends Base { } roles.push(role.id); } + options.roles = roles; } return this.client.api.guilds(this.id).members(user).put({ data: options }) .then(data => this.members.add(data)); diff --git a/src/structures/MessageCollector.js b/src/structures/MessageCollector.js index f357722b5..f8f3d5ab8 100644 --- a/src/structures/MessageCollector.js +++ b/src/structures/MessageCollector.js @@ -36,9 +36,9 @@ class MessageCollector extends Collector { */ this.received = 0; - const bulkDeleteListener = (messages => { + const bulkDeleteListener = messages => { for (const message of messages.values()) this.handleDispose(message); - }).bind(this); + }; this._handleChannelDeletion = this._handleChannelDeletion.bind(this); this._handleGuildDeletion = this._handleGuildDeletion.bind(this); From 90aa5b3500a2ba819db1c4facb65f9949b38fbc3 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Fri, 24 Jan 2020 11:08:40 -0500 Subject: [PATCH 1274/1359] feat(GuildMemberStore): make timeout refresh after every GUILD_MEMBERS_CHUNK (#3645) --- src/stores/GuildMemberStore.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index e02aa3dcc..7abc81caf 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -207,6 +207,7 @@ class GuildMemberStore extends DataStore { const fetchedMembers = new Collection(); const handler = (members, guild) => { if (guild.id !== this.guild.id) return; + timeout.refresh(); for (const member of members.values()) { if (query || limit) fetchedMembers.set(member.id, member); } @@ -217,11 +218,11 @@ class GuildMemberStore extends DataStore { resolve(query || limit ? fetchedMembers : this); } }; - this.guild.client.on(Events.GUILD_MEMBERS_CHUNK, handler); - this.guild.client.setTimeout(() => { + const timeout = this.guild.client.setTimeout(() => { this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); reject(new Error('GUILD_MEMBERS_TIMEOUT')); }, 120e3); + this.guild.client.on(Events.GUILD_MEMBERS_CHUNK, handler); }); } } From 3ea9ac57ddb6fa48ba26c5e181488bfcdef6b48a Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 25 Jan 2020 14:08:25 +0000 Subject: [PATCH 1275/1359] fix(ClientUser): verified and enabled properties resetting (#3733) * fix(ClientUser) verified and enabled properties resetting * set this.mfaEnabled to null if it is undefined * add missing curly brackets * fix typo --- src/structures/ClientUser.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 3f2883efc..a9512a646 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -16,17 +16,23 @@ class ClientUser extends Structures.get('User') { _patch(data) { super._patch(data); - /** - * Whether or not this account has been verified - * @type {boolean} - */ - this.verified = data.verified; + if ('verified' in data) { + /** + * Whether or not this account has been verified + * @type {boolean} + */ + this.verified = data.verified; + } - /** - * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account - * @type {?boolean} - */ - this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + if ('mfa_enabled' in data) { + /** + * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account + * @type {?boolean} + */ + this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + } else if (typeof this.mfaEnabled === 'undefined') { + this.mfaEnabled = null; + } if (data.token) this.client.token = data.token; } From d8b4725caa2e188e16c94f20ff7b3e5b821589ad Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Sun, 26 Jan 2020 01:27:39 +1100 Subject: [PATCH 1276/1359] fix(TextChannel#bulkDelete): use GenericAction#getMessage to handle return value correctly (#3664) * Corrected the handling of the action * Apply same fix to handling of single message in bulkDelete * Revert to using await --- src/structures/interfaces/TextBasedChannel.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 634dff33a..a6b183753 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -306,19 +306,16 @@ class TextBasedChannel { if (messageIDs.length === 0) return new Collection(); if (messageIDs.length === 1) { await this.client.api.channels(this.id).messages(messageIDs[0]).delete(); - const message = this.client.actions.MessageDelete.handle({ - channel_id: this.id, - id: messageIDs[0], - }).message; - if (message) return new Collection([[message.id, message]]); - return new Collection(); + const message = this.client.actions.MessageDelete.getMessage({ + message_id: messageIDs[0], + }, this); + return message ? new Collection([[message.id, message]]) : new Collection(); } await this.client.api.channels[this.id].messages['bulk-delete'] .post({ data: { messages: messageIDs } }); - return this.client.actions.MessageDeleteBulk.handle({ - channel_id: this.id, - ids: messageIDs, - }).messages; + return messageIDs.reduce((col, id) => col.set(id, this.client.actions.MessageDeleteBulk.getMessage({ + message_id: id, + }, this)), new Collection()); } if (!isNaN(messages)) { const msgs = await this.messages.fetch({ limit: messages }); From 030d263a9e018a766c5f399624fe4f8ec2b7349c Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Sat, 25 Jan 2020 14:00:53 -0500 Subject: [PATCH 1277/1359] feat(MessageReaction): add remove method and Client#messageReactionRemoveEmoji (#3723) * Add support for MessageReaction#remove and MESSAGE_REACTION_REMOVE_EMOJI * Remove reaction from cache Co-Authored-By: matthewfripp <50251454+matthewfripp@users.noreply.github.com> * fix: message may be partial * Clarify what the event entails * Document client in MessageReaction Co-Authored-By: SpaceEEC * await the REST call * Add MessageReaction#remove to typings Co-authored-by: matthewfripp <50251454+matthewfripp@users.noreply.github.com> Co-authored-by: SpaceEEC --- src/client/actions/Action.js | 2 +- src/client/actions/ActionsManager.js | 1 + .../actions/MessageReactionRemoveEmoji.js | 28 +++++++++++++++++++ .../handlers/MESSAGE_REACTION_REMOVE_EMOJI.js | 5 ++++ src/structures/MessageReaction.js | 17 +++++++++++ src/util/Constants.js | 2 ++ typings/index.d.ts | 2 ++ 7 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/client/actions/MessageReactionRemoveEmoji.js create mode 100644 src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index bc9ed267a..ce4fd6db2 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -54,7 +54,7 @@ class GenericAction { return this.getPayload({ emoji: data.emoji, count: message.partial ? null : 0, - me: user.id === this.client.user.id, + me: user ? user.id === this.client.user.id : false, }, message.reactions, id, PartialTypes.REACTION); } diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 730f507c0..7e1df121f 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -11,6 +11,7 @@ class ActionsManager { this.register(require('./MessageReactionAdd')); this.register(require('./MessageReactionRemove')); this.register(require('./MessageReactionRemoveAll')); + this.register(require('./MessageReactionRemoveEmoji')); this.register(require('./ChannelCreate')); this.register(require('./ChannelDelete')); this.register(require('./ChannelUpdate')); diff --git a/src/client/actions/MessageReactionRemoveEmoji.js b/src/client/actions/MessageReactionRemoveEmoji.js new file mode 100644 index 000000000..ab0eaa770 --- /dev/null +++ b/src/client/actions/MessageReactionRemoveEmoji.js @@ -0,0 +1,28 @@ +'use strict'; + +const Action = require('./Action'); +const { Events } = require('../../util/Constants'); + +class MessageReactionRemoveEmoji extends Action { + handle(data) { + const channel = this.getChannel(data); + if (!channel || channel.type === 'voice') return false; + + const message = this.getMessage(data, channel); + if (!message) return false; + + const reaction = this.getReaction(data, message); + if (!reaction) return false; + if (!message.partial) message.reactions.delete(reaction.emoji.id || reaction.emoji.name); + + /** + * Emitted when a bot removes an emoji reaction from a cached message. + * @event Client#messageReactionRemoveEmoji + * @param {MessageReaction} reaction The reaction that was removed + */ + this.client.emit(Events.MESSAGE_REACTION_REMOVE_EMOJI, reaction); + return { reaction }; + } +} + +module.exports = MessageReactionRemoveEmoji; diff --git a/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js new file mode 100644 index 000000000..579444c97 --- /dev/null +++ b/src/client/websocket/handlers/MESSAGE_REACTION_REMOVE_EMOJI.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = (client, packet) => { + client.actions.MessageReactionRemoveEmoji.handle(packet.d); +}; diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index f32f0843c..5cb9210fb 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -15,6 +15,13 @@ class MessageReaction { * @param {Message} message The message the reaction refers to */ constructor(client, data, message) { + /** + * The client that instantiated this message reaction + * @name MessageReaction#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); /** * The message that this reaction refers to * @type {Message} @@ -47,6 +54,16 @@ class MessageReaction { if (this.count == undefined) this.count = data.count; } + /** + * Removes all users from this reaction. + * @returns {Promise} + */ + async remove() { + await this.client.api.channels(this.message.channel.id).messages(this.message.id).reactions(this._emoji.identifier) + .delete(); + return this; + } + /** * The emoji of this reaction, either an GuildEmoji object for known custom emojis, or a ReactionEmoji * object which has fewer properties. Whatever the prototype of the emoji, it will still have diff --git a/src/util/Constants.js b/src/util/Constants.js index a8f93a856..0417d881c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -262,6 +262,7 @@ exports.Events = { MESSAGE_REACTION_ADD: 'messageReactionAdd', MESSAGE_REACTION_REMOVE: 'messageReactionRemove', MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', + MESSAGE_REACTION_REMOVE_EMOJI: 'messageReactionRemoveEmoji', USER_UPDATE: 'userUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_SERVER_UPDATE: 'voiceServerUpdate', @@ -379,6 +380,7 @@ exports.WSEvents = keyMirror([ 'MESSAGE_REACTION_ADD', 'MESSAGE_REACTION_REMOVE', 'MESSAGE_REACTION_REMOVE_ALL', + 'MESSAGE_REACTION_REMOVE_EMOJI', 'USER_UPDATE', 'PRESENCE_UPDATE', 'TYPING_START', diff --git a/typings/index.d.ts b/typings/index.d.ts index fc23f027f..8154f3f0f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -191,6 +191,7 @@ declare module 'discord.js' { public on(event: 'inviteCreate' | 'inviteDelete', listener: (invite: Invite) => void): this; public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; + public on(event: 'messageReactionRemoveEmoji', listener: (reaction: MessageReaction) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public on(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; public on(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; @@ -1113,6 +1114,7 @@ declare module 'discord.js' { public message: Message; public readonly partial: boolean; public users: ReactionUserStore; + public remove(): Promise; public fetch(): Promise; public toJSON(): object; } From 8e9e93da1d9c96a7acf63cc1c5317a42e56afbd8 Mon Sep 17 00:00:00 2001 From: Jyguy Date: Wed, 29 Jan 2020 12:54:10 -0500 Subject: [PATCH 1278/1359] docs(Guild): fetchBan returns a promise (#3752) --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 5827ff49b..04d091b0c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -557,7 +557,7 @@ class Guild extends Base { /** * Fetches information on a banned user from this guild. * @param {UserResolvable} user The User to fetch the ban info of - * @returns {BanInfo} + * @returns {Promise} */ fetchBan(user) { const id = this.client.users.resolveID(user); From 6a381c68a28ac6961dcf51b0e7d0f5d7d163e8a6 Mon Sep 17 00:00:00 2001 From: PLASMAchicken Date: Fri, 31 Jan 2020 12:38:10 +0100 Subject: [PATCH 1279/1359] chore(README): update link to Discord.js guide v12 changes (#3751) * Update link to discord.js guide v12 changes * Suggested Changes * Suggested Changes Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ec09092d..3a2957981 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ client.login('token'); * [Website](https://discord.js.org/) ([source](https://github.com/discordjs/website)) * [Documentation](https://discord.js.org/#/docs/main/master/general/welcome) * [Guide](https://discordjs.guide/) ([source](https://github.com/discordjs/guide)) - this is still for stable - See also the WIP [Update Guide](https://github.com/discordjs/guide/blob/v12-changes/guide/additional-info/changes-in-v12.md) also including updated and removed items in the library. + See also the [Update Guide](https://discordjs.guide/additional-info/changes-in-v12.html), including updated and removed items in the library. * [Discord.js Discord server](https://discord.gg/bRCvFy9) * [Discord API Discord server](https://discord.gg/discord-api) * [GitHub](https://github.com/discordjs/discord.js) From b4e56d3e0e8c3bf242d6bf52af03b0826af62bd8 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 2 Feb 2020 12:12:58 +0200 Subject: [PATCH 1280/1359] src: fix up WebSocketShard errors (#3722) * src: Fix up WebSocketShard errors * typings: Forgot to update * src: Forgot debug variable * src: Fix issue Bella found If the WS was not connected when the HELLO timeout passes (CONNECTING, etc), the shard would get stuck due to never rejecting the WebSocketShard#connect Promise with the DESTROYED event --- src/client/websocket/WebSocketManager.js | 8 +- src/client/websocket/WebSocketShard.js | 118 +++++++++++++++-------- typings/index.d.ts | 3 +- 3 files changed, 85 insertions(+), 44 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 1c22a5bfe..3e299c58d 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -18,7 +18,7 @@ const BeforeReadyWhitelist = [ WSEvents.GUILD_MEMBER_REMOVE, ]; -const UNRECOVERABLE_CLOSE_CODES = [4004, 4010, 4011]; +const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1); const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; /** @@ -235,7 +235,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Session ID is present, attempting an immediate reconnect...`, shard); this.reconnect(true); } else { - shard.destroy(undefined, true); + shard.destroy({ reset: true, emit: false, log: false }); this.reconnect(); } }); @@ -245,8 +245,6 @@ class WebSocketManager extends EventEmitter { }); shard.on(ShardEvents.DESTROYED, () => { - shard._cleanupConnection(); - this.debug('Shard was destroyed but no WebSocket connection was present! Reconnecting...', shard); this.client.emit(Events.SHARD_RECONNECTING, shard.id); @@ -342,7 +340,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); this.destroyed = true; this.shardQueue.clear(); - for (const shard of this.shards.values()) shard.destroy(1000, true); + for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false }); } /** diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index 1b7d5495b..b313c751a 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -178,7 +178,8 @@ class WebSocketShard extends EventEmitter { this.off(ShardEvents.CLOSE, onClose); this.off(ShardEvents.READY, onReady); this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.INVALID_SESSION, onInvalid); + this.off(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.off(ShardEvents.DESTROYED, onInvalidOrDestroyed); }; const onReady = () => { @@ -196,7 +197,7 @@ class WebSocketShard extends EventEmitter { reject(event); }; - const onInvalid = () => { + const onInvalidOrDestroyed = () => { cleanup(); // eslint-disable-next-line prefer-promise-reject-errors reject(); @@ -205,7 +206,8 @@ class WebSocketShard extends EventEmitter { this.once(ShardEvents.READY, onReady); this.once(ShardEvents.RESUMED, onResumed); this.once(ShardEvents.CLOSE, onClose); - this.once(ShardEvents.INVALID_SESSION, onInvalid); + this.once(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.once(ShardEvents.DESTROYED, onInvalidOrDestroyed); if (this.connection && this.connection.readyState === WebSocket.OPEN) { this.debug('An open connection was found, attempting an immediate identify.'); @@ -214,10 +216,9 @@ class WebSocketShard extends EventEmitter { } if (this.connection) { - this.debug(`A connection was found. Cleaning up before continuing. + this.debug(`A connection object was found. Cleaning up before continuing. State: ${CONNECTION_STATE[this.connection.readyState]}`); - this._cleanupConnection(); - this.connection.close(1000); + this.destroy({ emit: false }); } const wsQuery = { v: client.options.ws.version }; @@ -233,9 +234,9 @@ class WebSocketShard extends EventEmitter { this.debug( `[CONNECT] - Gateway: ${gateway} - Version: ${client.options.ws.version} - Encoding: ${WebSocket.encoding} + Gateway : ${gateway} + Version : ${client.options.ws.version} + Encoding : ${WebSocket.encoding} Compression: ${zlib ? 'zlib-stream' : 'none'}`); this.status = this.status === Status.DISCONNECTED ? Status.RECONNECTING : Status.CONNECTING; @@ -338,11 +339,13 @@ class WebSocketShard extends EventEmitter { this.debug(`[CLOSE] Event Code: ${event.code} - Clean: ${event.wasClean} - Reason: ${event.reason || 'No reason received'}`); + Clean : ${event.wasClean} + Reason : ${event.reason || 'No reason received'}`); this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); + // If we still have a connection object, clean up its listeners + if (this.connection) this._cleanupConnection(); this.status = Status.DISCONNECTED; @@ -362,7 +365,7 @@ class WebSocketShard extends EventEmitter { */ onPacket(packet) { if (!packet) { - this.debug(`Received broken packet: ${packet}.`); + this.debug(`Received broken packet: '${packet}'.`); return; } @@ -406,7 +409,7 @@ class WebSocketShard extends EventEmitter { this.identify(); break; case OPCodes.RECONNECT: - this.connection.close(1001); + this.destroy({ closeCode: 4000 }); break; case OPCodes.INVALID_SESSION: this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`); @@ -418,7 +421,7 @@ class WebSocketShard extends EventEmitter { // Reset the sequence this.sequence = -1; // Reset the session ID as it's invalid - this.sessionID = null; + this.sessionID = undefined; // Set the status to reconnecting this.status = Status.RECONNECTING; // Finally, emit the INVALID_SESSION event @@ -495,7 +498,7 @@ class WebSocketShard extends EventEmitter { this.debug('Setting a HELLO timeout for 20s.'); this.helloTimeout = this.manager.client.setTimeout(() => { this.debug('Did not receive HELLO in time. Destroying and connecting again.'); - this.destroy(4009); + this.destroy({ reset: true, closeCode: 4009 }); }, 20000); } @@ -535,9 +538,9 @@ class WebSocketShard extends EventEmitter { `[${tag}] Didn't receive a heartbeat ack last time, assuming zombie connection. Destroying and reconnecting. Status : ${STATUS_KEYS[this.status]} Sequence : ${this.sequence} - Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}` - ); - this.destroy(4009); + Connection State: ${this.connection ? CONNECTION_STATE[this.connection.readyState] : 'No Connection??'}`); + + this.destroy({ closeCode: 4009, reset: true }); return; } @@ -636,8 +639,8 @@ class WebSocketShard extends EventEmitter { */ _send(data) { if (!this.connection || this.connection.readyState !== WebSocket.OPEN) { - this.debug(`Tried to send packet ${JSON.stringify(data)} but no WebSocket is available! Resetting the shard...`); - this.destroy(4000); + this.debug(`Tried to send packet '${JSON.stringify(data)}' but no WebSocket is available!`); + this.destroy({ close: 4000 }); return; } @@ -670,35 +673,61 @@ class WebSocketShard extends EventEmitter { /** * Destroys this shard and closes its WebSocket connection. - * @param {number} [closeCode=1000] The close code to use - * @param {boolean} [cleanup=false] If the shard should attempt a reconnect + * @param {Object} [options={ closeCode: 1000, reset: false, emit: true, log: true }] Options for destroying the shard * @private */ - destroy(closeCode = 1000, cleanup = false) { - this.debug(`Destroying with close code ${closeCode}, attempting a reconnect: ${!cleanup}`); + destroy({ closeCode = 1000, reset = false, emit = true, log = true } = {}) { + if (log) { + this.debug(`[DESTROY] + Close Code : ${closeCode} + Reset : ${reset} + Emit DESTROYED: ${emit}`); + } + // Step 0: Remove all timers this.setHeartbeatTimer(-1); this.setHelloTimeout(-1); - // Close the WebSocket connection, if any - if (this.connection && this.connection.readyState === WebSocket.OPEN) { - this.connection.close(closeCode); - } else if (!cleanup) { - /** - * Emitted when a shard is destroyed, but no WebSocket connection was present. - * @private - * @event WebSocketShard#destroyed - */ - this.emit(ShardEvents.DESTROYED); + // Step 1: Close the WebSocket connection, if any, otherwise, emit DESTROYED + if (this.connection) { + // If the connection is currently opened, we will (hopefully) receive close + if (this.connection.readyState === WebSocket.OPEN) { + this.connection.close(closeCode); + } else { + // Connection is not OPEN + this.debug(`WS State: ${CONNECTION_STATE[this.connection.readyState]}`); + // Remove listeners from the connection + this._cleanupConnection(); + // Attempt to close the connection just in case + try { + this.connection.close(closeCode); + } catch { + // No-op + } + // Emit the destroyed event if needed + if (emit) this._emitDestroyed(); + } + } else if (emit) { + // We requested a destroy, but we had no connection. Emit destroyed + this._emitDestroyed(); } + // Step 2: Null the connection object this.connection = null; - // Set the shard status + + // Step 3: Set the shard status to DISCONNECTED this.status = Status.DISCONNECTED; + + // Step 4: Cache the old sequence (use to attempt a resume) if (this.sequence !== -1) this.closeSequence = this.sequence; - // Reset the sequence - this.sequence = -1; - // Reset the ratelimit data + + // Step 5: Reset the sequence and session ID if requested + if (reset) { + this.sequence = -1; + this.sessionID = undefined; + } + + // Step 6: reset the ratelimit data this.ratelimit.remaining = this.ratelimit.total; this.ratelimit.queue.length = 0; if (this.ratelimit.timer) { @@ -717,6 +746,19 @@ class WebSocketShard extends EventEmitter { this.connection.onerror = this.connection.onmessage = null; } + + /** + * Emits the DESTROYED event on the shard + * @private + */ + _emitDestroyed() { + /** + * Emitted when a shard is destroyed, but no WebSocket connection was present. + * @private + * @event WebSocketShard#destroyed + */ + this.emit(ShardEvents.DESTROYED); + } } module.exports = WebSocketShard; diff --git a/typings/index.d.ts b/typings/index.d.ts index 8154f3f0f..e59387f4b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1727,8 +1727,9 @@ declare module 'discord.js' { private identifyResume(): void; private _send(data: object): void; private processQueue(): void; - private destroy(closeCode: number): void; + private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean }): void; private _cleanupConnection(): void; + private _emitDestroyed(): void; public send(data: object): void; public on(event: 'ready', listener: () => void): this; From 3f039016af127c31cffe0733c3b4b31566a6d146 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 7 Feb 2020 18:27:05 +0100 Subject: [PATCH 1281/1359] fix(GuildMember): manageable - let owner override (#3765) * if the bot is owner of the guild the target is managebale * even though both roles are on the same position --- src/structures/GuildMember.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index dcd5a93dd..8a8b8bb6a 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -216,6 +216,7 @@ class GuildMember extends Base { get manageable() { if (this.user.id === this.guild.ownerID) return false; if (this.user.id === this.client.user.id) return false; + if (this.client.user.id === this.guild.ownerID) return true; if (!this.guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.guild.me.roles.highest.comparePositionTo(this.roles.highest) > 0; } From fe7df708e44e0280dfaf0f8e457b154781bb5140 Mon Sep 17 00:00:00 2001 From: didinele Date: Fri, 7 Feb 2020 19:46:03 +0200 Subject: [PATCH 1282/1359] typings: add HTTPOptions#api and export Constants as a value (#3768) * fix(typings): Export Constants correctly * fix(typings): HTTPOptions#api was missing * fix some odd indent * add semi to make CI happy uwu --- typings/index.d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index e59387f4b..07f725bce 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -372,7 +372,7 @@ declare module 'discord.js' { type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'gif'; - export interface Constants { + export const Constants: { Package: { name: string; version: string; @@ -627,7 +627,7 @@ declare module 'discord.js' { ActivityTypes: ActivityType[]; DefaultMessageNotifications: DefaultMessageNotifications[]; MembershipStates: 'INVITED' | 'ACCEPTED'; - } + }; export class DataResolver { public static resolveBase64(data: Base64Resolvable): string; @@ -2363,6 +2363,7 @@ declare module 'discord.js' { } interface HTTPOptions { + api?: string; version?: number; host?: string; cdn?: string; From bbdbc4cfa789383b7b3dbecf5e6b8401ea2dd998 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Tue, 11 Feb 2020 14:21:07 -0500 Subject: [PATCH 1283/1359] feat: remove datastores and implement Managers (#3696) * Initial commit: add 5 initial managers - Base manager - GuildChannelManager - MessageManager - PresenceManager - Reaction Manager - Added LimitedCollection * Add GuildEmojiManager, various fixes * Modify some managers and add guildmembermanager * Initial integration * Delete old stores * Integration part two, removed LRUCollection - Most of the integration has been finished - TODO typings - Removed LRUCollection, needless sweeping * Typings + stuff i somehow missed in ChannelManager * LimitedCollection typings/ final changes * Various jsdoc and syntactical fixes, Removed Util.mixin() * tslint fix * Grammatical and logical changes * Delete temporary file placed by mistake * Grammatical changes * Add missing type * Update jsdoc examples * fix: ChannelManager#remove should call cache#delete not cache#remove * fix recursive require * Fix missed cache in util * fix: more missed cache * Remove accidental _fetchMany change from #3645 * fix: use .cache.delete() over .remove() * fix: missing cache in ReactionCollector * fix: missed cache in client * fix: members is a collection not a manager Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com> * fix: various docs and cache fixes * fix: missed cache * fix: missing _roles * Final testing and debugging * LimitedCollection: return the Collection instead of undefined on .set * Add cache to BaseManager in typings * Commit fixes i forgot to stage yesterday * Update invite events * Account for new commit * fix: MessageReactionRemoveAll should call .cache.clear() * fix: add .cache at various places, correct return type * docs: remove mentions of 'store' * Add extra documented properties to typings Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> Co-authored-by: SpaceEEC --- docs/examples/greeting.js | 2 +- src/client/Client.js | 34 ++-- src/client/actions/Action.js | 6 +- src/client/actions/ChannelCreate.js | 2 +- src/client/actions/ChannelDelete.js | 4 +- src/client/actions/ChannelUpdate.js | 6 +- src/client/actions/GuildBanRemove.js | 2 +- .../actions/GuildChannelsPositionUpdate.js | 4 +- src/client/actions/GuildDelete.js | 8 +- src/client/actions/GuildEmojiDelete.js | 2 +- src/client/actions/GuildEmojisUpdate.js | 6 +- src/client/actions/GuildIntegrationsUpdate.js | 2 +- src/client/actions/GuildMemberRemove.js | 6 +- src/client/actions/GuildRoleCreate.js | 4 +- src/client/actions/GuildRoleDelete.js | 6 +- src/client/actions/GuildRoleUpdate.js | 4 +- .../actions/GuildRolesPositionUpdate.js | 4 +- src/client/actions/GuildUpdate.js | 2 +- src/client/actions/InviteCreate.js | 4 +- src/client/actions/InviteDelete.js | 4 +- src/client/actions/MessageCreate.js | 4 +- src/client/actions/MessageDelete.js | 2 +- src/client/actions/MessageDeleteBulk.js | 4 +- .../actions/MessageReactionRemoveAll.js | 2 +- .../actions/MessageReactionRemoveEmoji.js | 2 +- src/client/actions/PresenceUpdate.js | 8 +- src/client/actions/UserUpdate.js | 2 +- src/client/actions/VoiceStateUpdate.js | 8 +- src/client/actions/WebhooksUpdate.js | 2 +- src/client/voice/ClientVoiceManager.js | 2 +- src/client/voice/VoiceConnection.js | 2 +- src/client/websocket/WebSocketManager.js | 2 +- .../websocket/handlers/CHANNEL_PINS_UPDATE.js | 2 +- .../websocket/handlers/GUILD_BAN_ADD.js | 2 +- src/client/websocket/handlers/GUILD_CREATE.js | 2 +- .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 2 +- .../websocket/handlers/GUILD_MEMBER_ADD.js | 2 +- .../websocket/handlers/GUILD_MEMBER_UPDATE.js | 4 +- src/client/websocket/handlers/READY.js | 2 +- src/client/websocket/handlers/TYPING_START.js | 4 +- src/index.js | 29 ++-- .../DataStore.js => managers/BaseManager.js} | 59 ++++--- .../ChannelManager.js} | 73 +++------ .../GuildChannelManager.js} | 30 ++-- .../GuildEmojiManager.js} | 21 ++- .../GuildEmojiRoleManager.js} | 57 ++++--- .../GuildManager.js} | 23 ++- .../GuildMemberManager.js} | 33 ++-- .../GuildMemberRoleManager.js} | 62 +++---- src/managers/MessageManager.js | 141 ++++++++++++++++ .../PresenceManager.js} | 21 ++- .../ReactionManager.js} | 32 ++-- .../ReactionUserManager.js} | 25 ++- .../RoleStore.js => managers/RoleManager.js} | 39 +++-- .../UserStore.js => managers/UserManager.js} | 19 ++- src/managers/VoiceStateManager.js | 37 +++++ src/sharding/ShardClientUtil.js | 4 +- src/sharding/ShardingManager.js | 2 +- src/stores/MessageStore.js | 135 ---------------- src/stores/VoiceStateStore.js | 26 --- src/structures/CategoryChannel.js | 2 +- src/structures/Channel.js | 4 +- src/structures/DMChannel.js | 8 +- src/structures/Emoji.js | 2 +- src/structures/Guild.js | 85 +++++----- src/structures/GuildAuditLogs.js | 16 +- src/structures/GuildChannel.js | 8 +- src/structures/GuildEmoji.js | 16 +- src/structures/GuildMember.js | 20 +-- src/structures/Integration.js | 2 +- src/structures/Invite.js | 2 +- src/structures/Message.js | 12 +- src/structures/MessageMentions.js | 6 +- src/structures/MessageReaction.js | 18 +-- src/structures/Presence.js | 4 +- src/structures/ReactionCollector.js | 2 +- src/structures/Role.js | 2 +- src/structures/TextChannel.js | 8 +- src/structures/User.js | 10 +- src/structures/VoiceChannel.js | 2 +- src/structures/VoiceState.js | 4 +- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 10 +- src/util/LimitedCollection.js | 29 ++++ src/util/Structures.js | 2 +- src/util/Util.js | 38 +---- typings/index.d.ts | 153 +++++++++--------- 87 files changed, 804 insertions(+), 705 deletions(-) rename src/{stores/DataStore.js => managers/BaseManager.js} (50%) rename src/{stores/ChannelStore.js => managers/ChannelManager.js} (52%) rename src/{stores/GuildChannelStore.js => managers/GuildChannelManager.js} (85%) rename src/{stores/GuildEmojiStore.js => managers/GuildEmojiManager.js} (89%) rename src/{stores/GuildEmojiRoleStore.js => managers/GuildEmojiRoleManager.js} (73%) rename src/{stores/GuildStore.js => managers/GuildManager.js} (85%) rename src/{stores/GuildMemberStore.js => managers/GuildMemberManager.js} (91%) rename src/{stores/GuildMemberRoleStore.js => managers/GuildMemberRoleManager.js} (77%) create mode 100644 src/managers/MessageManager.js rename src/{stores/PresenceStore.js => managers/PresenceManager.js} (73%) rename src/{stores/ReactionStore.js => managers/ReactionManager.js} (74%) rename src/{stores/ReactionUserStore.js => managers/ReactionUserManager.js} (77%) rename src/{stores/RoleStore.js => managers/RoleManager.js} (78%) rename src/{stores/UserStore.js => managers/UserManager.js} (80%) create mode 100644 src/managers/VoiceStateManager.js delete mode 100644 src/stores/MessageStore.js delete mode 100644 src/stores/VoiceStateStore.js create mode 100644 src/util/LimitedCollection.js diff --git a/docs/examples/greeting.js b/docs/examples/greeting.js index 8fc1dfada..314a75986 100644 --- a/docs/examples/greeting.js +++ b/docs/examples/greeting.js @@ -19,7 +19,7 @@ client.on('ready', () => { // Create an event listener for new guild members client.on('guildMemberAdd', member => { // Send the message to a designated channel on a server: - const channel = member.guild.channels.find(ch => ch.name === 'member-log'); + const channel = member.guild.channels.cache.find(ch => ch.name === 'member-log'); // Do nothing if the channel wasn't found on this server if (!channel) return; // Send the message, mentioning the member diff --git a/src/client/Client.js b/src/client/Client.js index a1ab6672e..ae7bbf8b3 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -11,10 +11,10 @@ const Webhook = require('../structures/Webhook'); const Invite = require('../structures/Invite'); const ClientApplication = require('../structures/ClientApplication'); const ShardClientUtil = require('../sharding/ShardClientUtil'); -const UserStore = require('../stores/UserStore'); -const ChannelStore = require('../stores/ChannelStore'); -const GuildStore = require('../stores/GuildStore'); -const GuildEmojiStore = require('../stores/GuildEmojiStore'); +const UserManager = require('../managers/UserManager'); +const ChannelManager = require('../managers/ChannelManager'); +const GuildManager = require('../managers/GuildManager'); +const GuildEmojiManager = require('../managers/GuildEmojiManager'); const { Events, browser, DefaultOptions } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const Structures = require('../util/Structures'); @@ -99,25 +99,25 @@ class Client extends BaseClient { /** * All of the {@link User} objects that have been cached at any point, mapped by their IDs - * @type {UserStore} + * @type {UserManager} */ - this.users = new UserStore(this); + this.users = new UserManager(this); /** * All of the guilds the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* guild the bot is a member of - * @type {GuildStore} + * @type {GuildManager} */ - this.guilds = new GuildStore(this); + this.guilds = new GuildManager(this); /** * All of the {@link Channel}s that the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot * is a member of. Note that DM channels will not be initially cached, and thus not be present - * in the store without their explicit fetching or use. - * @type {ChannelStore} + * in the Manager without their explicit fetching or use. + * @type {ChannelManager} */ - this.channels = new ChannelStore(this); + this.channels = new ChannelManager(this); const ClientPresence = Structures.get('ClientPresence'); /** @@ -159,13 +159,13 @@ class Client extends BaseClient { /** * All custom emojis that the client has access to, mapped by their IDs - * @type {GuildEmojiStore} + * @type {GuildEmojiManager} * @readonly */ get emojis() { - const emojis = new GuildEmojiStore({ client: this }); - for (const guild of this.guilds.values()) { - if (guild.available) for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji); + const emojis = new GuildEmojiManager({ client: this }); + for (const guild of this.guilds.cache.values()) { + if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji); } return emojis; } @@ -298,11 +298,11 @@ class Client extends BaseClient { let channels = 0; let messages = 0; - for (const channel of this.channels.values()) { + for (const channel of this.channels.cache.values()) { if (!channel.messages) continue; channels++; - messages += channel.messages.sweep( + messages += channel.messages.cache.sweep( message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs ); } diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index ce4fd6db2..2a0bc861c 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -23,10 +23,10 @@ class GenericAction { return data; } - getPayload(data, store, id, partialType, cache) { - const existing = store.get(id); + getPayload(data, manager, id, partialType, cache) { + const existing = manager.cache.get(id); if (!existing && this.client.options.partials.includes(partialType)) { - return store.add(data, cache); + return manager.add(data, cache); } return existing; } diff --git a/src/client/actions/ChannelCreate.js b/src/client/actions/ChannelCreate.js index 6830f2ab5..fa60a0b39 100644 --- a/src/client/actions/ChannelCreate.js +++ b/src/client/actions/ChannelCreate.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class ChannelCreateAction extends Action { handle(data) { const client = this.client; - const existing = client.channels.has(data.id); + const existing = client.channels.cache.has(data.id); const channel = client.channels.add(data); if (!existing && channel) { /** diff --git a/src/client/actions/ChannelDelete.js b/src/client/actions/ChannelDelete.js index fdfc38709..66fad20db 100644 --- a/src/client/actions/ChannelDelete.js +++ b/src/client/actions/ChannelDelete.js @@ -12,13 +12,13 @@ class ChannelDeleteAction extends Action { handle(data) { const client = this.client; - let channel = client.channels.get(data.id); + let channel = client.channels.cache.get(data.id); if (channel) { client.channels.remove(channel.id); channel.deleted = true; if (channel.messages && !(channel instanceof DMChannel)) { - for (const message of channel.messages.values()) { + for (const message of channel.messages.cache.values()) { message.deleted = true; } } diff --git a/src/client/actions/ChannelUpdate.js b/src/client/actions/ChannelUpdate.js index 7b716de07..06bb71b85 100644 --- a/src/client/actions/ChannelUpdate.js +++ b/src/client/actions/ChannelUpdate.js @@ -8,16 +8,16 @@ class ChannelUpdateAction extends Action { handle(data) { const client = this.client; - let channel = client.channels.get(data.id); + let channel = client.channels.cache.get(data.id); if (channel) { const old = channel._update(data); if (ChannelTypes[channel.type.toUpperCase()] !== data.type) { const newChannel = Channel.create(this.client, data, channel.guild); - for (const [id, message] of channel.messages) newChannel.messages.set(id, message); + for (const [id, message] of channel.messages.cache) newChannel.messages.cache.set(id, message); newChannel._typing = new Map(channel._typing); channel = newChannel; - this.client.channels.set(channel.id, channel); + this.client.channels.cache.set(channel.id, channel); } return { diff --git a/src/client/actions/GuildBanRemove.js b/src/client/actions/GuildBanRemove.js index 5a4c0a902..fc28e113f 100644 --- a/src/client/actions/GuildBanRemove.js +++ b/src/client/actions/GuildBanRemove.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class GuildBanRemove extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); const user = client.users.add(data.user); /** * Emitted whenever a member is unbanned from a guild. diff --git a/src/client/actions/GuildChannelsPositionUpdate.js b/src/client/actions/GuildChannelsPositionUpdate.js index b21115947..a393167e6 100644 --- a/src/client/actions/GuildChannelsPositionUpdate.js +++ b/src/client/actions/GuildChannelsPositionUpdate.js @@ -6,10 +6,10 @@ class GuildChannelsPositionUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { for (const partialChannel of data.channels) { - const channel = guild.channels.get(partialChannel.id); + const channel = guild.channels.cache.get(partialChannel.id); if (channel) channel.rawPosition = partialChannel.position; } } diff --git a/src/client/actions/GuildDelete.js b/src/client/actions/GuildDelete.js index 193abc47c..69cd91438 100644 --- a/src/client/actions/GuildDelete.js +++ b/src/client/actions/GuildDelete.js @@ -12,9 +12,9 @@ class GuildDeleteAction extends Action { handle(data) { const client = this.client; - let guild = client.guilds.get(data.id); + let guild = client.guilds.cache.get(data.id); if (guild) { - for (const channel of guild.channels.values()) { + for (const channel of guild.channels.cache.values()) { if (channel.type === 'text') channel.stopTyping(true); } @@ -36,11 +36,11 @@ class GuildDeleteAction extends Action { }; } - for (const channel of guild.channels.values()) this.client.channels.remove(channel.id); + for (const channel of guild.channels.cache.values()) this.client.channels.remove(channel.id); if (guild.voice && guild.voice.connection) guild.voice.connection.disconnect(); // Delete guild - client.guilds.remove(guild.id); + client.guilds.cache.delete(guild.id); guild.deleted = true; /** diff --git a/src/client/actions/GuildEmojiDelete.js b/src/client/actions/GuildEmojiDelete.js index d5c973afa..42af70c12 100644 --- a/src/client/actions/GuildEmojiDelete.js +++ b/src/client/actions/GuildEmojiDelete.js @@ -5,7 +5,7 @@ const { Events } = require('../../util/Constants'); class GuildEmojiDeleteAction extends Action { handle(emoji) { - emoji.guild.emojis.remove(emoji.id); + emoji.guild.emojis.cache.delete(emoji.id); emoji.deleted = true; /** * Emitted whenever a custom emoji is deleted in a guild. diff --git a/src/client/actions/GuildEmojisUpdate.js b/src/client/actions/GuildEmojisUpdate.js index d6902d535..77119333d 100644 --- a/src/client/actions/GuildEmojisUpdate.js +++ b/src/client/actions/GuildEmojisUpdate.js @@ -4,14 +4,14 @@ const Action = require('./Action'); class GuildEmojisUpdateAction extends Action { handle(data) { - const guild = this.client.guilds.get(data.guild_id); + const guild = this.client.guilds.cache.get(data.guild_id); if (!guild || !guild.emojis) return; - const deletions = new Map(guild.emojis); + const deletions = new Map(guild.emojis.cache); for (const emoji of data.emojis) { // Determine type of emoji event - const cachedEmoji = guild.emojis.get(emoji.id); + const cachedEmoji = guild.emojis.cache.get(emoji.id); if (cachedEmoji) { deletions.delete(emoji.id); if (!cachedEmoji.equals(emoji)) { diff --git a/src/client/actions/GuildIntegrationsUpdate.js b/src/client/actions/GuildIntegrationsUpdate.js index a8e910774..4129be6ad 100644 --- a/src/client/actions/GuildIntegrationsUpdate.js +++ b/src/client/actions/GuildIntegrationsUpdate.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class GuildIntegrationsUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); /** * Emitted whenever a guild integration is updated * @event Client#guildIntegrationsUpdate diff --git a/src/client/actions/GuildMemberRemove.js b/src/client/actions/GuildMemberRemove.js index 28aa50386..ed6c0d144 100644 --- a/src/client/actions/GuildMemberRemove.js +++ b/src/client/actions/GuildMemberRemove.js @@ -6,14 +6,14 @@ const { Events, Status } = require('../../util/Constants'); class GuildMemberRemoveAction extends Action { handle(data, shard) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); let member = null; if (guild) { member = this.getMember(data, guild); guild.memberCount--; if (member) { member.deleted = true; - guild.members.remove(member.id); + guild.members.cache.delete(member.id); /** * Emitted whenever a member leaves a guild, or is kicked. * @event Client#guildMemberRemove @@ -21,7 +21,7 @@ class GuildMemberRemoveAction extends Action { */ if (shard.status === Status.READY) client.emit(Events.GUILD_MEMBER_REMOVE, member); } - guild.voiceStates.delete(data.user.id); + guild.voiceStates.cache.delete(data.user.id); } return { guild, member }; } diff --git a/src/client/actions/GuildRoleCreate.js b/src/client/actions/GuildRoleCreate.js index 36111f064..65c64f36a 100644 --- a/src/client/actions/GuildRoleCreate.js +++ b/src/client/actions/GuildRoleCreate.js @@ -6,10 +6,10 @@ const { Events } = require('../../util/Constants'); class GuildRoleCreate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); let role; if (guild) { - const already = guild.roles.has(data.role.id); + const already = guild.roles.cache.has(data.role.id); role = guild.roles.add(data.role); /** * Emitted whenever a role is created. diff --git a/src/client/actions/GuildRoleDelete.js b/src/client/actions/GuildRoleDelete.js index 31b13b812..ec9df8233 100644 --- a/src/client/actions/GuildRoleDelete.js +++ b/src/client/actions/GuildRoleDelete.js @@ -6,13 +6,13 @@ const { Events } = require('../../util/Constants'); class GuildRoleDeleteAction extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); let role; if (guild) { - role = guild.roles.get(data.role_id); + role = guild.roles.cache.get(data.role_id); if (role) { - guild.roles.remove(data.role_id); + guild.roles.cache.delete(data.role_id); role.deleted = true; /** * Emitted whenever a guild role is deleted. diff --git a/src/client/actions/GuildRoleUpdate.js b/src/client/actions/GuildRoleUpdate.js index bf61c7878..631746d2a 100644 --- a/src/client/actions/GuildRoleUpdate.js +++ b/src/client/actions/GuildRoleUpdate.js @@ -6,12 +6,12 @@ const { Events } = require('../../util/Constants'); class GuildRoleUpdateAction extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { let old = null; - const role = guild.roles.get(data.role.id); + const role = guild.roles.cache.get(data.role.id); if (role) { old = role._update(data.role); /** diff --git a/src/client/actions/GuildRolesPositionUpdate.js b/src/client/actions/GuildRolesPositionUpdate.js index f09f11436..d7abca97b 100644 --- a/src/client/actions/GuildRolesPositionUpdate.js +++ b/src/client/actions/GuildRolesPositionUpdate.js @@ -6,10 +6,10 @@ class GuildRolesPositionUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { for (const partialRole of data.roles) { - const role = guild.roles.get(partialRole.id); + const role = guild.roles.cache.get(partialRole.id); if (role) role.rawPosition = partialRole.position; } } diff --git a/src/client/actions/GuildUpdate.js b/src/client/actions/GuildUpdate.js index 6d7cf9b4e..c40fc0cde 100644 --- a/src/client/actions/GuildUpdate.js +++ b/src/client/actions/GuildUpdate.js @@ -7,7 +7,7 @@ class GuildUpdateAction extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.id); + const guild = client.guilds.cache.get(data.id); if (guild) { const old = guild._update(data); /** diff --git a/src/client/actions/InviteCreate.js b/src/client/actions/InviteCreate.js index 5552ea2f0..638133154 100644 --- a/src/client/actions/InviteCreate.js +++ b/src/client/actions/InviteCreate.js @@ -7,8 +7,8 @@ const { Events } = require('../../util/Constants'); class InviteCreateAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); - const guild = client.guilds.get(data.guild_id); + const channel = client.channels.cache.get(data.channel_id); + const guild = client.guilds.cache.get(data.guild_id); if (!channel && !guild) return false; const inviteData = Object.assign(data, { channel, guild }); diff --git a/src/client/actions/InviteDelete.js b/src/client/actions/InviteDelete.js index 83933d34f..92692c3e2 100644 --- a/src/client/actions/InviteDelete.js +++ b/src/client/actions/InviteDelete.js @@ -7,8 +7,8 @@ const { Events } = require('../../util/Constants'); class InviteDeleteAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); - const guild = client.guilds.get(data.guild_id); + const channel = client.channels.cache.get(data.channel_id); + const guild = client.guilds.cache.get(data.guild_id); if (!channel && !guild) return false; const inviteData = Object.assign(data, { channel, guild }); diff --git a/src/client/actions/MessageCreate.js b/src/client/actions/MessageCreate.js index 3772c4a10..ddb56ea97 100644 --- a/src/client/actions/MessageCreate.js +++ b/src/client/actions/MessageCreate.js @@ -6,9 +6,9 @@ const { Events } = require('../../util/Constants'); class MessageCreateAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); if (channel) { - const existing = channel.messages.get(data.id); + const existing = channel.messages.cache.get(data.id); if (existing) return { message: existing }; const message = channel.messages.add(data); const user = message.author; diff --git a/src/client/actions/MessageDelete.js b/src/client/actions/MessageDelete.js index feb118c10..7869365c9 100644 --- a/src/client/actions/MessageDelete.js +++ b/src/client/actions/MessageDelete.js @@ -11,7 +11,7 @@ class MessageDeleteAction extends Action { if (channel) { message = this.getMessage(data, channel); if (message) { - channel.messages.delete(message.id); + channel.messages.cache.delete(message.id); message.deleted = true; /** * Emitted whenever a message is deleted. diff --git a/src/client/actions/MessageDeleteBulk.js b/src/client/actions/MessageDeleteBulk.js index f80bc7c4d..00fd54b51 100644 --- a/src/client/actions/MessageDeleteBulk.js +++ b/src/client/actions/MessageDeleteBulk.js @@ -7,7 +7,7 @@ const { Events } = require('../../util/Constants'); class MessageDeleteBulkAction extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); if (channel) { const ids = data.ids; @@ -20,7 +20,7 @@ class MessageDeleteBulkAction extends Action { if (message) { message.deleted = true; messages.set(message.id, message); - channel.messages.delete(id); + channel.messages.cache.delete(id); } } diff --git a/src/client/actions/MessageReactionRemoveAll.js b/src/client/actions/MessageReactionRemoveAll.js index 0921ce50e..14b79bf08 100644 --- a/src/client/actions/MessageReactionRemoveAll.js +++ b/src/client/actions/MessageReactionRemoveAll.js @@ -13,7 +13,7 @@ class MessageReactionRemoveAll extends Action { const message = this.getMessage(data, channel); if (!message) return false; - message.reactions.clear(); + message.reactions.cache.clear(); this.client.emit(Events.MESSAGE_REACTION_REMOVE_ALL, message); return { message }; diff --git a/src/client/actions/MessageReactionRemoveEmoji.js b/src/client/actions/MessageReactionRemoveEmoji.js index ab0eaa770..143702c00 100644 --- a/src/client/actions/MessageReactionRemoveEmoji.js +++ b/src/client/actions/MessageReactionRemoveEmoji.js @@ -13,7 +13,7 @@ class MessageReactionRemoveEmoji extends Action { const reaction = this.getReaction(data, message); if (!reaction) return false; - if (!message.partial) message.reactions.delete(reaction.emoji.id || reaction.emoji.name); + if (!message.partial) message.reactions.cache.delete(reaction.emoji.id || reaction.emoji.name); /** * Emitted when a bot removes an emoji reaction from a cached message. diff --git a/src/client/actions/PresenceUpdate.js b/src/client/actions/PresenceUpdate.js index 538789e25..f74fbeb5d 100644 --- a/src/client/actions/PresenceUpdate.js +++ b/src/client/actions/PresenceUpdate.js @@ -5,7 +5,7 @@ const { Events } = require('../../util/Constants'); class PresenceUpdateAction extends Action { handle(data) { - let user = this.client.users.get(data.user.id); + let user = this.client.users.cache.get(data.user.id); if (!user && data.user.username) user = this.client.users.add(data.user); if (!user) return; @@ -13,12 +13,12 @@ class PresenceUpdateAction extends Action { if (!user.equals(data.user)) this.client.actions.UserUpdate.handle(data.user); } - const guild = this.client.guilds.get(data.guild_id); + const guild = this.client.guilds.cache.get(data.guild_id); if (!guild) return; - let oldPresence = guild.presences.get(user.id); + let oldPresence = guild.presences.cache.get(user.id); if (oldPresence) oldPresence = oldPresence._clone(); - let member = guild.members.get(user.id); + let member = guild.members.cache.get(user.id); if (!member && data.status !== 'offline') { member = guild.members.add({ user, diff --git a/src/client/actions/UserUpdate.js b/src/client/actions/UserUpdate.js index a762511f4..7279ca7dd 100644 --- a/src/client/actions/UserUpdate.js +++ b/src/client/actions/UserUpdate.js @@ -7,7 +7,7 @@ class UserUpdateAction extends Action { handle(data) { const client = this.client; - const newUser = client.users.get(data.id); + const newUser = client.users.cache.get(data.id); const oldUser = newUser._update(data); if (!oldUser.equals(newUser)) { diff --git a/src/client/actions/VoiceStateUpdate.js b/src/client/actions/VoiceStateUpdate.js index 386bf7783..b2a4b11bd 100644 --- a/src/client/actions/VoiceStateUpdate.js +++ b/src/client/actions/VoiceStateUpdate.js @@ -7,17 +7,17 @@ const VoiceState = require('../../structures/VoiceState'); class VoiceStateUpdate extends Action { handle(data) { const client = this.client; - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { // Update the state - const oldState = guild.voiceStates.has(data.user_id) ? - guild.voiceStates.get(data.user_id)._clone() : + const oldState = guild.voiceStates.cache.has(data.user_id) ? + guild.voiceStates.cache.get(data.user_id)._clone() : new VoiceState(guild, { user_id: data.user_id }); const newState = guild.voiceStates.add(data); // Get the member - let member = guild.members.get(data.user_id); + let member = guild.members.cache.get(data.user_id); if (member && data.member) { member._patch(data.member); } else if (data.member && data.member.user && data.member.joined_at) { diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 69e28aecb..f6a515b2c 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -6,7 +6,7 @@ const { Events } = require('../../util/Constants'); class WebhooksUpdate extends Action { handle(data) { const client = this.client; - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); /** * Emitted whenever a guild text channel has its webhooks changed. * @event Client#webhookUpdate diff --git a/src/client/voice/ClientVoiceManager.js b/src/client/voice/ClientVoiceManager.js index cf7526623..fbf5d1ba1 100644 --- a/src/client/voice/ClientVoiceManager.js +++ b/src/client/voice/ClientVoiceManager.js @@ -56,7 +56,7 @@ class ClientVoiceManager { this.connections.delete(guild_id); return; } - connection.channel = this.client.channels.get(channel_id); + connection.channel = this.client.channels.cache.get(channel_id); connection.setSessionID(session_id); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index b4b1e9da1..050775b60 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -483,7 +483,7 @@ class VoiceConnection extends EventEmitter { onSpeaking({ user_id, speaking }) { speaking = new Speaking(speaking).freeze(); const guild = this.channel.guild; - const user = this.client.users.get(user_id); + const user = this.client.users.cache.get(user_id); const old = this._speaking.get(user_id); this._speaking.set(user_id, speaking); /** diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 3e299c58d..02139f484 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -408,7 +408,7 @@ class WebSocketManager extends EventEmitter { if (this.client.options.fetchAllMembers) { try { - const promises = this.client.guilds.map(guild => { + const promises = this.client.guilds.cache.map(guild => { if (guild.available) return guild.members.fetch(); // Return empty promise if guild is unavailable return Promise.resolve(); diff --git a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js index 6da643ff3..13e6f0faf 100644 --- a/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js +++ b/src/client/websocket/handlers/CHANNEL_PINS_UPDATE.js @@ -3,7 +3,7 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - const channel = client.channels.get(data.channel_id); + const channel = client.channels.cache.get(data.channel_id); const time = new Date(data.last_pin_timestamp); if (channel && !Number.isNaN(time.getTime())) { diff --git a/src/client/websocket/handlers/GUILD_BAN_ADD.js b/src/client/websocket/handlers/GUILD_BAN_ADD.js index cbb60e13c..5d4a0965c 100644 --- a/src/client/websocket/handlers/GUILD_BAN_ADD.js +++ b/src/client/websocket/handlers/GUILD_BAN_ADD.js @@ -3,7 +3,7 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); const user = client.users.add(data.user); /** diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 33cc0c239..eb8974435 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -3,7 +3,7 @@ const { Events, Status } = require('../../../util/Constants'); module.exports = async (client, { d: data }, shard) => { - let guild = client.guilds.get(data.id); + let guild = client.guilds.cache.get(data.id); if (guild) { if (!guild.available && !data.unavailable) { // A newly available guild diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js index 0738eaa67..9a0a880a7 100644 --- a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -4,7 +4,7 @@ const { Events } = require('../../../util/Constants'); const Collection = require('../../../util/Collection'); module.exports = (client, { d: data }) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (!guild) return; const members = new Collection(); diff --git a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js index 796b6c376..964e3da26 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_ADD.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -3,7 +3,7 @@ const { Events, Status } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { guild.memberCount++; const member = guild.members.add(data); diff --git a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js index 9341329a3..92c9da6c5 100644 --- a/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js +++ b/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -3,9 +3,9 @@ const { Status, Events } = require('../../../util/Constants'); module.exports = (client, { d: data }, shard) => { - const guild = client.guilds.get(data.guild_id); + const guild = client.guilds.cache.get(data.guild_id); if (guild) { - const member = guild.members.get(data.user.id); + const member = guild.members.cache.get(data.user.id); if (member) { const old = member._update(data); if (shard.status === Status.READY) { diff --git a/src/client/websocket/handlers/READY.js b/src/client/websocket/handlers/READY.js index 002575023..c38b681c4 100644 --- a/src/client/websocket/handlers/READY.js +++ b/src/client/websocket/handlers/READY.js @@ -9,7 +9,7 @@ module.exports = (client, { d: data }, shard) => { if (!ClientUser) ClientUser = require('../../../structures/ClientUser'); const clientUser = new ClientUser(client, data.user); client.user = clientUser; - client.users.set(clientUser.id, clientUser); + client.users.cache.set(clientUser.id, clientUser); } for (const guild of data.guilds) { diff --git a/src/client/websocket/handlers/TYPING_START.js b/src/client/websocket/handlers/TYPING_START.js index 9df76dc75..92e0125f2 100644 --- a/src/client/websocket/handlers/TYPING_START.js +++ b/src/client/websocket/handlers/TYPING_START.js @@ -3,8 +3,8 @@ const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - const channel = client.channels.get(data.channel_id); - const user = client.users.get(data.user_id); + const channel = client.channels.cache.get(data.channel_id); + const user = client.users.cache.get(data.user_id); if (channel && user) { /** diff --git a/src/index.js b/src/index.js index 896919c40..155d2f539 100644 --- a/src/index.js +++ b/src/index.js @@ -17,7 +17,8 @@ module.exports = { Collection: require('./util/Collection'), Constants: require('./util/Constants'), DataResolver: require('./util/DataResolver'), - DataStore: require('./stores/DataStore'), + LimitedCollection: require('./util/LimitedCollection'), + BaseManager: require('./managers/BaseManager'), DiscordAPIError: require('./rest/DiscordAPIError'), HTTPError: require('./rest/HTTPError'), MessageFlags: require('./util/MessageFlags'), @@ -30,19 +31,19 @@ module.exports = { Util: Util, version: require('../package.json').version, - // Stores - ChannelStore: require('./stores/ChannelStore'), - GuildChannelStore: require('./stores/GuildChannelStore'), - GuildEmojiStore: require('./stores/GuildEmojiStore'), - GuildEmojiRoleStore: require('./stores/GuildEmojiRoleStore'), - GuildMemberStore: require('./stores/GuildMemberStore'), - GuildMemberRoleStore: require('./stores/GuildMemberRoleStore'), - GuildStore: require('./stores/GuildStore'), - ReactionUserStore: require('./stores/ReactionUserStore'), - MessageStore: require('./stores/MessageStore'), - PresenceStore: require('./stores/PresenceStore'), - RoleStore: require('./stores/RoleStore'), - UserStore: require('./stores/UserStore'), + // Managers + ChannelManager: require('./managers/ChannelManager'), + GuildChannelManager: require('./managers/GuildChannelManager'), + GuildEmojiManager: require('./managers/GuildEmojiManager'), + GuildEmojiRoleManager: require('./managers/GuildEmojiRoleManager'), + GuildMemberManager: require('./managers/GuildMemberManager'), + GuildMemberRoleManager: require('./managers/GuildMemberRoleManager'), + GuildManager: require('./managers/GuildManager'), + ReactionUserManager: require('./managers/ReactionUserManager'), + MessageManager: require('./managers/MessageManager'), + PresenceManager: require('./managers/PresenceManager'), + RoleManager: require('./managers/RoleManager'), + UserManager: require('./managers/UserManager'), // Shortcuts to Util methods discordSort: Util.discordSort, diff --git a/src/stores/DataStore.js b/src/managers/BaseManager.js similarity index 50% rename from src/stores/DataStore.js rename to src/managers/BaseManager.js index be1e93d79..b9d0be938 100644 --- a/src/stores/DataStore.js +++ b/src/managers/BaseManager.js @@ -4,44 +4,67 @@ const Collection = require('../util/Collection'); let Structures; /** - * Manages the creation, retrieval and deletion of a specific data model. - * @extends {Collection} + * Manages the API methods of a data model and holds its cache. + * @abstract */ -class DataStore extends Collection { - constructor(client, iterable, holds) { - super(); +class BaseManager { + constructor(client, iterable, holds, cacheType = Collection, ...cacheOptions) { if (!Structures) Structures = require('../util/Structures'); - Object.defineProperty(this, 'client', { value: client }); + /** + * The data structure belonging to this manager + * @name BaseManager#holds + * @type {Function} + * @private + * @readonly + */ Object.defineProperty(this, 'holds', { value: Structures.get(holds.name) || holds }); - if (iterable) for (const item of iterable) this.add(item); + + /** + * The client that instantiated this Manager + * @name BaseManager#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The type of Collection of the Manager + * @type {Collection} + */ + this.cacheType = cacheType; + + /** + * Holds the cache for the data model + * @type {?Collection} + */ + this.cache = new cacheType(...cacheOptions); + if (iterable) for (const i of iterable) this.add(i); } add(data, cache = true, { id, extras = [] } = {}) { - const existing = this.get(id || data.id); + const existing = this.cache.get(id || data.id); if (existing && existing._patch && cache) existing._patch(data); if (existing) return existing; const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; - if (cache) this.set(id || entry.id, entry); + if (cache) this.cache.set(id || entry.id, entry); return entry; } - remove(key) { return this.delete(key); } - /** * Resolves a data entry to a data Object. - * @param {string|Object} idOrInstance The id or instance of something in this DataStore - * @returns {?Object} An instance from this DataStore + * @param {string|Object} idOrInstance The id or instance of something in this Manager + * @returns {?Object} An instance from this Manager */ resolve(idOrInstance) { if (idOrInstance instanceof this.holds) return idOrInstance; - if (typeof idOrInstance === 'string') return this.get(idOrInstance) || null; + if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) || null; return null; } /** * Resolves a data entry to a instance ID. - * @param {string|Instance} idOrInstance The id or instance of something in this DataStore + * @param {string|Instance} idOrInstance The id or instance of something in this Manager * @returns {?Snowflake} */ resolveID(idOrInstance) { @@ -49,10 +72,6 @@ class DataStore extends Collection { if (typeof idOrInstance === 'string') return idOrInstance; return null; } - - static get [Symbol.species]() { - return Collection; - } } -module.exports = DataStore; +module.exports = BaseManager; diff --git a/src/stores/ChannelStore.js b/src/managers/ChannelManager.js similarity index 52% rename from src/stores/ChannelStore.js rename to src/managers/ChannelManager.js index 762f0e193..dcb098c13 100644 --- a/src/stores/ChannelStore.js +++ b/src/managers/ChannelManager.js @@ -1,59 +1,26 @@ 'use strict'; -const DataStore = require('./DataStore'); const Channel = require('../structures/Channel'); +const BaseManager = require('./BaseManager'); const { Events } = require('../util/Constants'); -const kLru = Symbol('LRU'); -const lruable = ['dm']; - /** - * Stores channels. - * @extends {DataStore} + * A manager of channels belonging to a client */ -class ChannelStore extends DataStore { - constructor(client, iterableOrOptions = {}, options) { - if (!options && typeof iterableOrOptions[Symbol.iterator] !== 'function') { - options = iterableOrOptions; - iterableOrOptions = undefined; - } - super(client, iterableOrOptions, Channel); - - if (options.lru) { - const lru = this[kLru] = []; - lru.add = item => { - lru.remove(item); - lru.unshift(item); - while (lru.length > options.lru) this.remove(lru[lru.length - 1]); - }; - lru.remove = item => { - const index = lru.indexOf(item); - if (index > -1) lru.splice(index, 1); - }; - } +class ChannelManager extends BaseManager { + constructor(client, iterable) { + super(client, iterable, Channel); } - get(key, peek = false) { - const item = super.get(key); - if (!item || !lruable.includes(item.type)) return item; - if (!peek && this[kLru]) this[kLru].add(key); - return item; - } - - set(key, val) { - if (this[kLru] && lruable.includes(val.type)) this[kLru].add(key); - return super.set(key, val); - } - - delete(key) { - const item = this.get(key, true); - if (!item) return false; - if (this[kLru] && lruable.includes(item.type)) this[kLru].remove(key); - return super.delete(key); - } + /** + * The cache of Channels + * @property {Collection} cache + * @memberof ChannelManager + * @instance + */ add(data, guild, cache = true) { - const existing = this.get(data.id); + const existing = this.cache.get(data.id); if (existing) { if (existing._patch && cache) existing._patch(data); if (guild) guild.channels.add(existing); @@ -67,15 +34,15 @@ class ChannelStore extends DataStore { return null; } - if (cache) this.set(channel.id, channel); + if (cache) this.cache.set(channel.id, channel); return channel; } remove(id) { - const channel = this.get(id); - if (channel.guild) channel.guild.channels.remove(id); - super.remove(id); + const channel = this.cache.get(id); + if (channel.guild) channel.guild.channels.cache.delete(id); + this.cache.delete(id); } /** @@ -88,7 +55,7 @@ class ChannelStore extends DataStore { /** * Resolves a ChannelResolvable to a Channel object. * @method resolve - * @memberof ChannelStore + * @memberof ChannelManager * @instance * @param {ChannelResolvable} channel The channel resolvable to resolve * @returns {?Channel} @@ -97,7 +64,7 @@ class ChannelStore extends DataStore { /** * Resolves a ChannelResolvable to a channel ID string. * @method resolveID - * @memberof ChannelStore + * @memberof ChannelManager * @instance * @param {ChannelResolvable} channel The channel resolvable to resolve * @returns {?Snowflake} @@ -115,7 +82,7 @@ class ChannelStore extends DataStore { * .catch(console.error); */ async fetch(id, cache = true) { - const existing = this.get(id); + const existing = this.cache.get(id); if (existing && !existing.partial) return existing; const data = await this.client.api.channels(id).get(); @@ -123,4 +90,4 @@ class ChannelStore extends DataStore { } } -module.exports = ChannelStore; +module.exports = ChannelManager; diff --git a/src/stores/GuildChannelStore.js b/src/managers/GuildChannelManager.js similarity index 85% rename from src/stores/GuildChannelStore.js rename to src/managers/GuildChannelManager.js index 552b40f00..147a0b207 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/managers/GuildChannelManager.js @@ -1,24 +1,36 @@ 'use strict'; const { ChannelTypes } = require('../util/Constants'); -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const GuildChannel = require('../structures/GuildChannel'); const PermissionOverwrites = require('../structures/PermissionOverwrites'); /** - * Stores guild channels. - * @extends {DataStore} + * Manages API methods for GuildChannels and stores their cache. + * @extends {BaseManager} */ -class GuildChannelStore extends DataStore { +class GuildChannelManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, GuildChannel); + + /** + * The guild this Manager belongs to + * @type {Guild} + */ this.guild = guild; } + /** + * The cache of this Manager + * @property {Collection} cache + * @memberof GuildChannelManager + * @instance + */ + add(channel) { - const existing = this.get(channel.id); + const existing = this.cache.get(channel.id); if (existing) return existing; - this.set(channel.id, channel); + this.cache.set(channel.id, channel); return channel; } @@ -32,7 +44,7 @@ class GuildChannelStore extends DataStore { /** * Resolves a GuildChannelResolvable to a Channel object. * @method resolve - * @memberof GuildChannelStore + * @memberof GuildChannelManager * @instance * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve * @returns {?Channel} @@ -41,7 +53,7 @@ class GuildChannelStore extends DataStore { /** * Resolves a GuildChannelResolvable to a channel ID string. * @method resolveID - * @memberof GuildChannelStore + * @memberof GuildChannelManager * @instance * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve * @returns {?Snowflake} @@ -117,4 +129,4 @@ class GuildChannelStore extends DataStore { } } -module.exports = GuildChannelStore; +module.exports = GuildChannelManager; diff --git a/src/stores/GuildEmojiStore.js b/src/managers/GuildEmojiManager.js similarity index 89% rename from src/stores/GuildEmojiStore.js rename to src/managers/GuildEmojiManager.js index b992a68c9..27ac363ca 100644 --- a/src/stores/GuildEmojiStore.js +++ b/src/managers/GuildEmojiManager.js @@ -1,22 +1,33 @@ 'use strict'; const Collection = require('../util/Collection'); -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const GuildEmoji = require('../structures/GuildEmoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); const DataResolver = require('../util/DataResolver'); const { TypeError } = require('../errors'); /** - * Stores guild emojis. - * @extends {DataStore} + * Manages API methods for GuildEmojis and stores their cache. + * @extends {BaseManager} */ -class GuildEmojiStore extends DataStore { +class GuildEmojiManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, GuildEmoji); + /** + * The guild this manager belongs to + * @type {Guild} + */ this.guild = guild; } + /** + * The cache of GuildEmojis + * @property {Collection} cache + * @memberof GuildEmojiManager + * @instance + */ + add(data, cache) { return super.add(data, cache, { extras: [this.guild] }); } @@ -114,4 +125,4 @@ class GuildEmojiStore extends DataStore { } } -module.exports = GuildEmojiStore; +module.exports = GuildEmojiManager; diff --git a/src/stores/GuildEmojiRoleStore.js b/src/managers/GuildEmojiRoleManager.js similarity index 73% rename from src/stores/GuildEmojiRoleStore.js rename to src/managers/GuildEmojiRoleManager.js index 6db2003ed..577595588 100644 --- a/src/stores/GuildEmojiRoleStore.js +++ b/src/managers/GuildEmojiRoleManager.js @@ -1,18 +1,28 @@ 'use strict'; const Collection = require('../util/Collection'); -const Util = require('../util/Util'); const { TypeError } = require('../errors'); /** - * Stores emoji roles - * @extends {Collection} + * Manages API methods for roles belonging to emojis and stores their cache. */ -class GuildEmojiRoleStore extends Collection { +class GuildEmojiRoleManager { constructor(emoji) { - super(); + /** + * The emoji belonging to this manager + * @type {GuildEmoji} + */ this.emoji = emoji; + /** + * The guild belonging to this manager + * @type {Guild} + */ this.guild = emoji.guild; + /** + * The client belonging to this manager + * @type {Client} + * @readonly + */ Object.defineProperty(this, 'client', { value: emoji.client }); } @@ -22,8 +32,17 @@ class GuildEmojiRoleStore extends Collection { * @private * @readonly */ - get _filtered() { - return this.guild.roles.filter(role => this.emoji._roles.includes(role.id)); + get _roles() { + return this.guild.roles.cache.filter(role => this.emoji._roles.includes(role.id)); + } + + /** + * The cache of roles belonging to this emoji + * @type {Collection} + * @readonly + */ + get cache() { + return this._roles; } /** @@ -41,7 +60,7 @@ class GuildEmojiRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true)); } - const newRoles = [...new Set(roleOrRoles.concat(...this.values()))]; + const newRoles = [...new Set(roleOrRoles.concat(...this._roles.values()))]; return this.set(newRoles); } @@ -60,7 +79,7 @@ class GuildEmojiRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true)); } - const newRoles = this.keyArray().filter(role => !roleOrRoles.includes(role)); + const newRoles = this._roles.keyArray().filter(role => !roleOrRoles.includes(role)); return this.set(newRoles); } @@ -85,32 +104,18 @@ class GuildEmojiRoleStore extends Collection { clone() { const clone = new this.constructor(this.emoji); - clone._patch(this.keyArray().slice()); + clone._patch(this._roles.keyArray().slice()); return clone; } /** - * Patches the roles for this store + * Patches the roles for this manager's cache * @param {Snowflake[]} roles The new roles * @private */ _patch(roles) { this.emoji._roles = roles; } - - *[Symbol.iterator]() { - yield* this._filtered.entries(); - } - - valueOf() { - return this._filtered; - } - - static get [Symbol.species]() { - return Collection; - } } -Util.mixin(GuildEmojiRoleStore, ['set']); - -module.exports = GuildEmojiRoleStore; +module.exports = GuildEmojiRoleManager; diff --git a/src/stores/GuildStore.js b/src/managers/GuildManager.js similarity index 85% rename from src/stores/GuildStore.js rename to src/managers/GuildManager.js index eb7090b63..5666156b5 100644 --- a/src/stores/GuildStore.js +++ b/src/managers/GuildManager.js @@ -1,6 +1,6 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const DataResolver = require('../util/DataResolver'); const { Events } = require('../util/Constants'); const Guild = require('../structures/Guild'); @@ -9,14 +9,21 @@ const GuildMember = require('../structures/GuildMember'); const Role = require('../structures/Role'); /** - * Stores guilds. - * @extends {DataStore} + * Manages API methods for Guilds and stores their cache. + * @extends {BaseManager} */ -class GuildStore extends DataStore { +class GuildManager extends BaseManager { constructor(client, iterable) { super(client, iterable, Guild); } + /** + * The cache of this Manager + * @property {Collection} cache + * @memberof GuildManager + * @instance + */ + /** * Data that resolves to give a Guild object. This can be: * * A Guild object @@ -29,7 +36,7 @@ class GuildStore extends DataStore { /** * Resolves a GuildResolvable to a Guild object. * @method resolve - * @memberof GuildStore + * @memberof GuildManager * @instance * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Guild} @@ -44,7 +51,7 @@ class GuildStore extends DataStore { /** * Resolves a GuildResolvable to a Guild ID string. * @method resolveID - * @memberof GuildStore + * @memberof GuildManager * @instance * @param {GuildResolvable} guild The guild resolvable to identify * @returns {?Snowflake} @@ -70,7 +77,7 @@ class GuildStore extends DataStore { return new Promise((resolve, reject) => this.client.api.guilds.post({ data: { name, region, icon } }) .then(data => { - if (this.client.guilds.has(data.id)) return resolve(this.client.guilds.get(data.id)); + if (this.client.guilds.cache.has(data.id)) return resolve(this.client.guilds.cache.get(data.id)); const handleGuild = guild => { if (guild.id === data.id) { @@ -95,4 +102,4 @@ class GuildStore extends DataStore { } } -module.exports = GuildStore; +module.exports = GuildManager; diff --git a/src/stores/GuildMemberStore.js b/src/managers/GuildMemberManager.js similarity index 91% rename from src/stores/GuildMemberStore.js rename to src/managers/GuildMemberManager.js index 7abc81caf..b7d49f5c3 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/managers/GuildMemberManager.js @@ -1,21 +1,32 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const GuildMember = require('../structures/GuildMember'); const { Events, OPCodes } = require('../util/Constants'); const Collection = require('../util/Collection'); const { Error, TypeError } = require('../errors'); /** - * Stores guild members. - * @extends {DataStore} + * Manages API methods for GuildMembers and stores their cache. + * @extends {BaseManager} */ -class GuildMemberStore extends DataStore { +class GuildMemberManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, GuildMember); + /** + * The guild this manager belongs to + * @type {Guild} + */ this.guild = guild; } + /** + * The cache of this Manager + * @property {Collection} cache + * @memberof GuildMemberManager + * @instance + */ + add(data, cache = true) { return super.add(data, cache, { id: data.user.id, extras: [this.guild] }); } @@ -49,7 +60,7 @@ class GuildMemberStore extends DataStore { const memberResolvable = super.resolveID(member); if (memberResolvable) return memberResolvable; const userResolvable = this.client.users.resolveID(member); - return this.has(userResolvable) ? userResolvable : null; + return this.cache.has(userResolvable) ? userResolvable : null; } /** @@ -184,7 +195,7 @@ class GuildMemberStore extends DataStore { _fetchSingle({ user, cache }) { - const existing = this.get(user); + const existing = this.cache.get(user); if (existing && !existing.partial) return Promise.resolve(existing); return this.client.api.guilds(this.guild.id).members(user).get() .then(data => this.add(data, cache)); @@ -192,8 +203,8 @@ class GuildMemberStore extends DataStore { _fetchMany({ query = '', limit = 0 } = {}) { return new Promise((resolve, reject) => { - if (this.guild.memberCount === this.size && !query && !limit) { - resolve(this); + if (this.guild.memberCount === this.cache.size && !query && !limit) { + resolve(this.cache); return; } this.guild.shard.send({ @@ -211,11 +222,11 @@ class GuildMemberStore extends DataStore { for (const member of members.values()) { if (query || limit) fetchedMembers.set(member.id, member); } - if (this.guild.memberCount <= this.size || + if (this.guild.memberCount <= this.cache.size || ((query || limit) && members.size < 1000) || (limit && fetchedMembers.size >= limit)) { this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); - resolve(query || limit ? fetchedMembers : this); + resolve(query || limit ? fetchedMembers : this.cache); } }; const timeout = this.guild.client.setTimeout(() => { @@ -227,4 +238,4 @@ class GuildMemberStore extends DataStore { } } -module.exports = GuildMemberStore; +module.exports = GuildMemberManager; diff --git a/src/stores/GuildMemberRoleStore.js b/src/managers/GuildMemberRoleManager.js similarity index 77% rename from src/stores/GuildMemberRoleStore.js rename to src/managers/GuildMemberRoleManager.js index 047b8086a..d9186f90e 100644 --- a/src/stores/GuildMemberRoleStore.js +++ b/src/managers/GuildMemberRoleManager.js @@ -1,17 +1,22 @@ 'use strict'; const Collection = require('../util/Collection'); -const Util = require('../util/Util'); const { TypeError } = require('../errors'); /** - * Stores member roles - * @extends {Collection} + * Manages API methods for roles of a GuildMember and stores their cache. */ -class GuildMemberRoleStore extends Collection { +class GuildMemberRoleManager { constructor(member) { - super(); + /** + * The GuildMember this manager belongs to + * @type {GuildMember} + */ this.member = member; + /** + * The Guild this manager belongs to + * @type {Guild} + */ this.guild = member.guild; Object.defineProperty(this, 'client', { value: member.client }); } @@ -22,9 +27,18 @@ class GuildMemberRoleStore extends Collection { * @private * @readonly */ - get _filtered() { + get _roles() { const everyone = this.guild.roles.everyone; - return this.guild.roles.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); + return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); + } + + /** + * The roles of this member + * @type {Collection} + * @readonly + */ + get cache() { + return this._roles; } /** @@ -33,7 +47,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get hoist() { - const hoistedRoles = this._filtered.filter(role => role.hoist); + const hoistedRoles = this._roles.filter(role => role.hoist); if (!hoistedRoles.size) return null; return hoistedRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); } @@ -44,7 +58,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get color() { - const coloredRoles = this._filtered.filter(role => role.color); + const coloredRoles = this._roles.filter(role => role.color); if (!coloredRoles.size) return null; return coloredRoles.reduce((prev, role) => !prev || role.comparePositionTo(prev) > 0 ? role : prev); } @@ -55,7 +69,7 @@ class GuildMemberRoleStore extends Collection { * @readonly */ get highest() { - return this._filtered.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this.first()); + return this._roles.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this._roles.first()); } /** @@ -72,7 +86,7 @@ class GuildMemberRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true); } - const newRoles = [...new Set(roleOrRoles.concat(...this.values()))]; + const newRoles = [...new Set(roleOrRoles.concat(...this._roles.values()))]; return this.set(newRoles, reason); } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); @@ -84,7 +98,7 @@ class GuildMemberRoleStore extends Collection { await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].put({ reason }); const clone = this.member._clone(); - clone._roles = [...this.keys(), roleOrRoles.id]; + clone._roles = [...this._roles.keys(), roleOrRoles.id]; return clone; } } @@ -103,7 +117,7 @@ class GuildMemberRoleStore extends Collection { 'Array or Collection of Roles or Snowflakes', true); } - const newRoles = this.filter(role => !roleOrRoles.includes(role)); + const newRoles = this._roles.filter(role => !roleOrRoles.includes(role)); return this.set(newRoles, reason); } else { roleOrRoles = this.guild.roles.resolve(roleOrRoles); @@ -115,7 +129,7 @@ class GuildMemberRoleStore extends Collection { await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles.id].delete({ reason }); const clone = this.member._clone(); - const newRoles = this.filter(role => role.id !== roleOrRoles.id); + const newRoles = this._roles.filter(role => role.id !== roleOrRoles.id); clone._roles = [...newRoles.keys()]; return clone; } @@ -134,7 +148,7 @@ class GuildMemberRoleStore extends Collection { * @example * // Remove all the roles from a member * guildMember.roles.set([]) - * .then(member => console.log(`Member roles is now of ${member.roles.size} size`)) + * .then(member => console.log(`Member roles is now of ${member.roles.cache.size} size`)) * .catch(console.error); */ set(roles, reason) { @@ -143,23 +157,9 @@ class GuildMemberRoleStore extends Collection { clone() { const clone = new this.constructor(this.member); - clone.member._roles = [...this.keyArray()]; + clone.member._roles = [...this._roles.keyArray()]; return clone; } - - *[Symbol.iterator]() { - yield* this._filtered.entries(); - } - - valueOf() { - return this._filtered; - } - - static get [Symbol.species]() { - return Collection; - } } -Util.mixin(GuildMemberRoleStore, ['set']); - -module.exports = GuildMemberRoleStore; +module.exports = GuildMemberRoleManager; diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js new file mode 100644 index 000000000..19eb82b8c --- /dev/null +++ b/src/managers/MessageManager.js @@ -0,0 +1,141 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const Message = require('../structures/Message'); +const LimitedCollection = require('../util/LimitedCollection'); +const Collection = require('../util/Collection'); + +/** +* Manages API methods for Messages and holds their cache. +* @extends {BaseManager} +*/ +class MessageManager extends BaseManager { + constructor(channel, iterable) { + super(channel.client, iterable, Message, LimitedCollection, channel.client.options.messageCacheMaxSize); + /** + * The channel that the messages belong to + * @type {TextBasedChannel} + */ + this.channel = channel; + } + + /** + * The cache of Messages + * @property {LimitedCollection} cache + * @memberof MessageManager + * @instance + */ + + add(data, cache) { + return super.add(data, cache, { extras: [this.channel] }); + } + + /** + * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and + * `after` are mutually exclusive. All the parameters are optional. + * @typedef {Object} ChannelLogsQueryOptions + * @property {number} [limit=50] Number of messages to acquire + * @property {Snowflake} [before] ID of a message to get the messages that were posted before it + * @property {Snowflake} [after] ID of a message to get the messages that were posted after it + * @property {Snowflake} [around] ID of a message to get the messages that were posted around it + */ + + /** + * Gets a message, or messages, from this channel. + * The returned Collection does not contain reaction users of the messages if they were not cached. + * Those need to be fetched separately in such a case. + * @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters. + * @param {boolean} [cache=true] Whether to cache the message(s) + * @returns {Promise|Promise>} + * @example + * // Get message + * channel.messages.fetch('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + * @example + * // Get messages + * channel.messages.fetch({ limit: 10 }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Get messages and filter by user ID + * channel.messages.fetch() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) + * .catch(console.error); + */ + fetch(message, cache = true) { + return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache); + } + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately. + * @param {boolean} [cache=true] Whether to cache the message(s) + * @returns {Promise>} + * @example + * // Get pinned messages + * channel.fetchPinned() + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + fetchPinned(cache = true) { + return this.client.api.channels[this.channel.id].pins.get().then(data => { + const messages = new Collection(); + for (const message of data) messages.set(message.id, this.add(message, cache)); + return messages; + }); + } + + /** + * Data that can be resolved to a Message object. This can be: + * * A Message + * * A Snowflake + * @typedef {Message|Snowflake} MessageResolvable + */ + + /** + * Resolves a MessageResolvable to a Message object. + * @method resolve + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Message} + */ + + /** + * Resolves a MessageResolvable to a Message ID string. + * @method resolveID + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Snowflake} + */ + + + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user + */ + async delete(message, reason) { + message = this.resolveID(message); + if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); + } + + async _fetchId(messageID, cache) { + const existing = this.cache.get(messageID); + if (existing && !existing.partial) return existing; + const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); + return this.add(data, cache); + } + + async _fetchMany(options = {}, cache) { + const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this.add(message, cache)); + return messages; + } +} + +module.exports = MessageManager; diff --git a/src/stores/PresenceStore.js b/src/managers/PresenceManager.js similarity index 73% rename from src/stores/PresenceStore.js rename to src/managers/PresenceManager.js index 061d3f1e9..0a38fd874 100644 --- a/src/stores/PresenceStore.js +++ b/src/managers/PresenceManager.js @@ -1,19 +1,26 @@ 'use strict'; -const DataStore = require('./DataStore'); const { Presence } = require('../structures/Presence'); +const BaseManager = require('./BaseManager'); /** - * Stores presences. - * @extends {DataStore} + * Manages API methods for Presences and holds their cache. + * @extends {BaseManager} */ -class PresenceStore extends DataStore { +class PresenceManager extends BaseManager { constructor(client, iterable) { super(client, iterable, Presence); } + /** + * The cache of Presences + * @property {Collection} cache + * @memberof PresenceManager + * @instance + */ + add(data, cache) { - const existing = this.get(data.user.id); + const existing = this.cache.get(data.user.id); return existing ? existing.patch(data) : super.add(data, cache, { id: data.user.id }); } @@ -46,8 +53,8 @@ class PresenceStore extends DataStore { const presenceResolvable = super.resolveID(presence); if (presenceResolvable) return presenceResolvable; const userResolvable = this.client.users.resolveID(presence); - return this.has(userResolvable) ? userResolvable : null; + return this.cache.has(userResolvable) ? userResolvable : null; } } -module.exports = PresenceStore; +module.exports = PresenceManager; diff --git a/src/stores/ReactionStore.js b/src/managers/ReactionManager.js similarity index 74% rename from src/stores/ReactionStore.js rename to src/managers/ReactionManager.js index 1b1fb6030..7350aebda 100644 --- a/src/stores/ReactionStore.js +++ b/src/managers/ReactionManager.js @@ -1,15 +1,20 @@ 'use strict'; -const DataStore = require('./DataStore'); const MessageReaction = require('../structures/MessageReaction'); +const BaseManager = require('./BaseManager'); /** - * Stores reactions. - * @extends {DataStore} + * Manages API methods for reactions and holds their cache. + * @extends {BaseManager} */ -class ReactionStore extends DataStore { +class ReactionManager extends BaseManager { constructor(message, iterable) { super(message.client, iterable, MessageReaction); + + /** + * The message that this manager belongs to + * @type {Message} + */ this.message = message; } @@ -17,6 +22,13 @@ class ReactionStore extends DataStore { return super.add(data, cache, { id: data.emoji.id || data.emoji.name, extras: [this.message] }); } + /** + * The reaction cache of this manager + * @property {Collection} cache + * @memberof ReactionManager + * @instance + */ + /** * Data that can be resolved to a MessageReaction object. This can be: * * A MessageReaction @@ -27,7 +39,7 @@ class ReactionStore extends DataStore { /** * Resolves a MessageReactionResolvable to a MessageReaction object. * @method resolve - * @memberof ReactionStore + * @memberof ReactionManager * @instance * @param {MessageReactionResolvable} reaction The MessageReaction to resolve * @returns {?MessageReaction} @@ -36,7 +48,7 @@ class ReactionStore extends DataStore { /** * Resolves a MessageReactionResolvable to a MessageReaction ID string. * @method resolveID - * @memberof ReactionStore + * @memberof ReactionManager * @instance * @param {MessageReactionResolvable} reaction The MessageReaction to resolve * @returns {?Snowflake} @@ -53,18 +65,18 @@ class ReactionStore extends DataStore { _partial(emoji) { const id = emoji.id || emoji.name; - const existing = this.get(id); + const existing = this.cache.get(id); return !existing || existing.partial; } async _fetchReaction(reactionEmoji, cache) { const id = reactionEmoji.id || reactionEmoji.name; - const existing = this.get(id); + const existing = this.cache.get(id); if (!this._partial(reactionEmoji)) return existing; const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { reactionEmoji.reaction._patch({ count: 0 }); - this.message.reactions.remove(id); + this.message.reactions.cache.delete(id); return existing; } for (const reaction of data.reactions) { @@ -74,4 +86,4 @@ class ReactionStore extends DataStore { } } -module.exports = ReactionStore; +module.exports = ReactionManager; diff --git a/src/stores/ReactionUserStore.js b/src/managers/ReactionUserManager.js similarity index 77% rename from src/stores/ReactionUserStore.js rename to src/managers/ReactionUserManager.js index dc250a9fb..c82b119d1 100644 --- a/src/stores/ReactionUserStore.js +++ b/src/managers/ReactionUserManager.js @@ -1,19 +1,30 @@ 'use strict'; const Collection = require('../util/Collection'); -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const { Error } = require('../errors'); /** - * A data store to store User models who reacted to a MessageReaction. - * @extends {DataStore} + * Manages API methods for users who reacted to a reaction and stores their cache. + * @extends {BaseManager} */ -class ReactionUserStore extends DataStore { +class ReactionUserManager extends BaseManager { constructor(client, iterable, reaction) { - super(client, iterable, require('../structures/User')); + super(client, iterable, { name: 'User' }); + /** + * The reaction that this manager belongs to + * @type {MessageReaction} + */ this.reaction = reaction; } + /** + * The cache of this manager + * @property {Collection} cache + * @memberof GuildManager + * @instance + */ + /** * Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their IDs. * @param {Object} [options] Options for fetching the users @@ -30,7 +41,7 @@ class ReactionUserStore extends DataStore { const users = new Collection(); for (const rawUser of data) { const user = this.client.users.add(rawUser); - this.set(user.id, user); + this.cache.set(user.id, user); users.set(user.id, user); } return users; @@ -52,4 +63,4 @@ class ReactionUserStore extends DataStore { } } -module.exports = ReactionUserStore; +module.exports = ReactionUserManager; diff --git a/src/stores/RoleStore.js b/src/managers/RoleManager.js similarity index 78% rename from src/stores/RoleStore.js rename to src/managers/RoleManager.js index 649048be2..9eb03cf2c 100644 --- a/src/stores/RoleStore.js +++ b/src/managers/RoleManager.js @@ -1,20 +1,31 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const Role = require('../structures/Role'); const { resolveColor } = require('../util/Util'); const Permissions = require('../util/Permissions'); /** - * Stores roles. - * @extends {DataStore} + * Manages API methods for roles and stores their cache. + * @extends {BaseManager} */ -class RoleStore extends DataStore { +class RoleManager extends BaseManager { constructor(guild, iterable) { super(guild.client, iterable, Role); + /** + * The guild belonging to this manager + * @type {Guild} + */ this.guild = guild; } + /** + * The role cache of this manager + * @property {Collection} cache + * @memberof RoleManager + * @instance + */ + add(data, cache) { return super.add(data, cache, { extras: [this.guild] }); } @@ -23,11 +34,11 @@ class RoleStore extends DataStore { * Obtains one or more roles from Discord, or the role cache if they're already available. * @param {Snowflake} [id] ID or IDs of the role(s) * @param {boolean} [cache=true] Whether to cache the new roles objects if it weren't already - * @returns {Promise} + * @returns {Promise} * @example * // Fetch all roles from the guild * message.guild.roles.fetch() - * .then(roles => console.log(`There are ${roles.size} roles.`)) + * .then(roles => console.log(`There are ${roles.cache.size} roles.`)) * .catch(console.error); * @example * // Fetch a single role @@ -37,14 +48,14 @@ class RoleStore extends DataStore { */ async fetch(id, cache = true) { if (id) { - const existing = this.get(id); + const existing = this.cache.get(id); if (existing) return existing; } // We cannot fetch a single role, as of this commit's date, Discord API throws with 405 const roles = await this.client.api.guilds(this.guild.id).roles.get(); for (const role of roles) this.add(role, cache); - return id ? this.get(id) || null : this; + return id ? this.cache.get(id) || null : this; } /** @@ -57,7 +68,7 @@ class RoleStore extends DataStore { /** * Resolves a RoleResolvable to a Role object. * @method resolve - * @memberof RoleStore + * @memberof RoleManager * @instance * @param {RoleResolvable} role The role resolvable to resolve * @returns {?Role} @@ -66,7 +77,7 @@ class RoleStore extends DataStore { /** * Resolves a RoleResolvable to a role ID string. * @method resolveID - * @memberof RoleStore + * @memberof RoleManager * @instance * @param {RoleResolvable} role The role resolvable to resolve * @returns {?Snowflake} @@ -116,17 +127,17 @@ class RoleStore extends DataStore { * @readonly */ get everyone() { - return this.get(this.guild.id) || null; + return this.cache.get(this.guild.id) || null; } /** - * The role with the highest position in the store + * The role with the highest position in the cache * @type {Role} * @readonly */ get highest() { - return this.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this.first()); + return this.cache.reduce((prev, role) => role.comparePositionTo(prev) > 0 ? role : prev, this.cache.first()); } } -module.exports = RoleStore; +module.exports = RoleManager; diff --git a/src/stores/UserStore.js b/src/managers/UserManager.js similarity index 80% rename from src/stores/UserStore.js rename to src/managers/UserManager.js index 20c25d05b..8818206aa 100644 --- a/src/stores/UserStore.js +++ b/src/managers/UserManager.js @@ -1,19 +1,26 @@ 'use strict'; -const DataStore = require('./DataStore'); +const BaseManager = require('./BaseManager'); const User = require('../structures/User'); const GuildMember = require('../structures/GuildMember'); const Message = require('../structures/Message'); /** - * A data store to store User models. - * @extends {DataStore} + * Manages API methods for users and stores their cache. + * @extends {BaseManager} */ -class UserStore extends DataStore { +class UserManager extends BaseManager { constructor(client, iterable) { super(client, iterable, User); } + /** + * The cache of this manager + * @property {Collection} cache + * @memberof UserManager + * @instance + */ + /** * Data that resolves to give a User object. This can be: * * A User object @@ -52,11 +59,11 @@ class UserStore extends DataStore { * @returns {Promise} */ async fetch(id, cache = true) { - const existing = this.get(id); + const existing = this.cache.get(id); if (existing && !existing.partial) return existing; const data = await this.client.api.users(id).get(); return this.add(data, cache); } } -module.exports = UserStore; +module.exports = UserManager; diff --git a/src/managers/VoiceStateManager.js b/src/managers/VoiceStateManager.js new file mode 100644 index 000000000..755392b2d --- /dev/null +++ b/src/managers/VoiceStateManager.js @@ -0,0 +1,37 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const VoiceState = require('../structures/VoiceState'); + +/** + * Manages API methods for VoiceStates and stores their cache. + * @extends {BaseManager} + */ +class VoiceStateManager extends BaseManager { + constructor(guild, iterable) { + super(guild.client, iterable, VoiceState); + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this manager + * @property {Collection} cache + * @memberof VoiceStateManager + * @instance + */ + + add(data, cache = true) { + const existing = this.cache.get(data.user_id); + if (existing) return existing._patch(data); + + const entry = new VoiceState(this.guild, data); + if (cache) this.cache.set(data.user_id, entry); + return entry; + } +} + +module.exports = VoiceStateManager; diff --git a/src/sharding/ShardClientUtil.js b/src/sharding/ShardClientUtil.js index 8ed1b9787..8b76efd6f 100644 --- a/src/sharding/ShardClientUtil.js +++ b/src/sharding/ShardClientUtil.js @@ -86,7 +86,7 @@ class ShardClientUtil { * @param {string} prop Name of the client property to get, using periods for nesting * @returns {Promise>} * @example - * client.shard.fetchClientValues('guilds.size') + * client.shard.fetchClientValues('guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); * @see {@link ShardingManager#fetchClientValues} @@ -114,7 +114,7 @@ class ShardClientUtil { * @param {string|Function} script JavaScript to run on each shard * @returns {Promise>} Results of the script execution * @example - * client.shard.broadcastEval('this.guilds.size') + * client.shard.broadcastEval('this.guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); * @see {@link ShardingManager#broadcastEval} diff --git a/src/sharding/ShardingManager.js b/src/sharding/ShardingManager.js index 7126148d0..b6818d306 100644 --- a/src/sharding/ShardingManager.js +++ b/src/sharding/ShardingManager.js @@ -229,7 +229,7 @@ class ShardingManager extends EventEmitter { * @param {string} prop Name of the client property to get, using periods for nesting * @returns {Promise>} * @example - * manager.fetchClientValues('guilds.size') + * manager.fetchClientValues('guilds.cache.size') * .then(results => console.log(`${results.reduce((prev, val) => prev + val, 0)} total guilds`)) * .catch(console.error); */ diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js deleted file mode 100644 index 59b224f72..000000000 --- a/src/stores/MessageStore.js +++ /dev/null @@ -1,135 +0,0 @@ -'use strict'; - -const DataStore = require('./DataStore'); -const Collection = require('../util/Collection'); -const Message = require('../structures/Message'); - -/** - * Stores messages for text-based channels. - * @extends {DataStore} - */ -class MessageStore extends DataStore { - constructor(channel, iterable) { - super(channel.client, iterable, Message); - this.channel = channel; - } - - add(data, cache) { - return super.add(data, cache, { extras: [this.channel] }); - } - - set(key, value) { - const maxSize = this.client.options.messageCacheMaxSize; - if (maxSize === 0) return; - if (this.size >= maxSize && maxSize > 0) this.delete(this.firstKey()); - super.set(key, value); - } - - /** - * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and - * `after` are mutually exclusive. All the parameters are optional. - * @typedef {Object} ChannelLogsQueryOptions - * @property {number} [limit=50] Number of messages to acquire - * @property {Snowflake} [before] ID of a message to get the messages that were posted before it - * @property {Snowflake} [after] ID of a message to get the messages that were posted after it - * @property {Snowflake} [around] ID of a message to get the messages that were posted around it - */ - - /** - * Gets a message, or messages, from this channel. - * The returned Collection does not contain reaction users of the messages if they were not cached. - * Those need to be fetched separately in such a case. - * @param {Snowflake|ChannelLogsQueryOptions} [message] The ID of the message to fetch, or query parameters. - * @param {boolean} [cache=true] Whether to cache the message(s) - * @returns {Promise|Promise>} - * @example - * // Get message - * channel.messages.fetch('99539446449315840') - * .then(message => console.log(message.content)) - * .catch(console.error); - * @example - * // Get messages - * channel.messages.fetch({ limit: 10 }) - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - * @example - * // Get messages and filter by user ID - * channel.messages.fetch() - * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) - * .catch(console.error); - */ - fetch(message, cache = true) { - return typeof message === 'string' ? this._fetchId(message, cache) : this._fetchMany(message, cache); - } - - /** - * Fetches the pinned messages of this channel and returns a collection of them. - * The returned Collection does not contain any reaction data of the messages. - * Those need to be fetched separately. - * @param {boolean} [cache=true] Whether to cache the message(s) - * @returns {Promise>} - * @example - * // Get pinned messages - * channel.fetchPinned() - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - */ - fetchPinned(cache = true) { - return this.client.api.channels[this.channel.id].pins.get().then(data => { - const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message, cache)); - return messages; - }); - } - - /** - * Data that can be resolved to a Message object. This can be: - * * A Message - * * A Snowflake - * @typedef {Message|Snowflake} MessageResolvable - */ - - /** - * Resolves a MessageResolvable to a Message object. - * @method resolve - * @memberof MessageStore - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Message} - */ - - /** - * Resolves a MessageResolvable to a Message ID string. - * @method resolveID - * @memberof MessageStore - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Snowflake} - */ - - /** - * Deletes a message, even if it's not cached. - * @param {MessageResolvable} message The message to delete - * @param {string} [reason] Reason for deleting this message, if it does not belong to the client user - */ - async remove(message, reason) { - message = this.resolveID(message); - if (message) await this.client.api.channels(this.channel.id).messages(message).delete({ reason }); - } - - async _fetchId(messageID, cache) { - const existing = this.get(messageID); - if (existing && !existing.partial) return existing; - const data = await this.client.api.channels[this.channel.id].messages[messageID].get(); - return this.add(data, cache); - } - - async _fetchMany(options = {}, cache) { - const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this.add(message, cache)); - return messages; - } -} - -module.exports = MessageStore; diff --git a/src/stores/VoiceStateStore.js b/src/stores/VoiceStateStore.js deleted file mode 100644 index a5eaac2dc..000000000 --- a/src/stores/VoiceStateStore.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const DataStore = require('./DataStore'); -const VoiceState = require('../structures/VoiceState'); - -/** - * Stores voice states. - * @extends {DataStore} - */ -class VoiceStateStore extends DataStore { - constructor(guild, iterable) { - super(guild.client, iterable, VoiceState); - this.guild = guild; - } - - add(data, cache = true) { - const existing = this.get(data.user_id); - if (existing) return existing._patch(data); - - const entry = new VoiceState(this.guild, data); - if (cache) this.set(data.user_id, entry); - return entry; - } -} - -module.exports = VoiceStateStore; diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index 60be408c6..4ac9fbbb2 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -13,7 +13,7 @@ class CategoryChannel extends GuildChannel { * @readonly */ get children() { - return this.guild.channels.filter(c => c.parentID === this.id); + return this.guild.channels.cache.filter(c => c.parentID === this.id); } /** diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 9eade6600..d5949c3cd 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -100,7 +100,7 @@ class Channel extends Base { const DMChannel = Structures.get('DMChannel'); channel = new DMChannel(client, data); } else { - guild = guild || client.guilds.get(data.guild_id); + guild = guild || client.guilds.cache.get(data.guild_id); if (guild) { switch (data.type) { case ChannelTypes.TEXT: { @@ -129,7 +129,7 @@ class Channel extends Base { break; } } - if (channel) guild.channels.set(channel.id, channel); + if (channel) guild.channels.cache.set(channel.id, channel); } } return channel; diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 006a9fab2..9042519a9 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -2,7 +2,7 @@ const Channel = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); -const MessageStore = require('../stores/MessageStore'); +const MessageManager = require('../managers/MessageManager'); /** * Represents a direct message channel between two users. @@ -19,10 +19,10 @@ class DMChannel extends Channel { // Override the channel type so partials have a known type this.type = 'dm'; /** - * A collection containing the messages sent to this channel - * @type {MessageStore} + * A manager of the messages belonging to this channel + * @type {MessageManager} */ - this.messages = new MessageStore(this); + this.messages = new MessageManager(this); this._typing = new Map(); } diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index 803fc5002..5cbf913d5 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -82,7 +82,7 @@ class Emoji extends Base { * @returns {string} * @example * // Send a custom emoji from a guild: - * const emoji = guild.emojis.first(); + * const emoji = guild.emojis.cache.first(); * msg.reply(`Hello! ${emoji}`); * @example * // Send the emoji used in a reaction to the channel the reaction is part of diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 04d091b0c..d9bad7cef 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -11,12 +11,12 @@ const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); const Snowflake = require('../util/Snowflake'); const SystemChannelFlags = require('../util/SystemChannelFlags'); -const GuildMemberStore = require('../stores/GuildMemberStore'); -const RoleStore = require('../stores/RoleStore'); -const GuildEmojiStore = require('../stores/GuildEmojiStore'); -const GuildChannelStore = require('../stores/GuildChannelStore'); -const PresenceStore = require('../stores/PresenceStore'); -const VoiceStateStore = require('../stores/VoiceStateStore'); +const GuildMemberManager = require('../managers/GuildMemberManager'); +const RoleManager = require('../managers/RoleManager'); +const GuildEmojiManager = require('../managers/GuildEmojiManager'); +const GuildChannelManager = require('../managers/GuildChannelManager'); +const PresenceManager = require('../managers/PresenceManager'); +const VoiceStateManager = require('../managers/VoiceStateManager'); const Base = require('./Base'); const { Error, TypeError } = require('../errors'); @@ -35,34 +35,34 @@ class Guild extends Base { super(client); /** - * A collection of members that are in this guild. The key is the member's ID, the value is the member - * @type {GuildMemberStore} + * A manager of the members belonging to this guild + * @type {GuildMemberManager} */ - this.members = new GuildMemberStore(this); + this.members = new GuildMemberManager(this); /** - * A collection of channels that are in this guild. The key is the channel's ID, the value is the channel - * @type {GuildChannelStore} + * A manager of the members belonging to this guild + * @type {GuildChannelManager} */ - this.channels = new GuildChannelStore(this); + this.channels = new GuildChannelManager(this); /** - * A collection of roles that are in this guild. The key is the role's ID, the value is the role - * @type {RoleStore} + * A manager of the roles belonging to this guild + * @type {RoleManager} */ - this.roles = new RoleStore(this); + this.roles = new RoleManager(this); /** - * A collection of presences in this guild - * @type {PresenceStore} + * A manager of the presences belonging to this guild + * @type {PresenceManager} */ - this.presences = new PresenceStore(this.client); + this.presences = new PresenceManager(this.client); /** - * A collection of voice states in this guild - * @type {VoiceStateStore} + * A manager of the voice states of this guild + * @type {VoiceStateManager} */ - this.voiceStates = new VoiceStateStore(this); + this.voiceStates = new VoiceStateManager(this); /** * Whether the bot has been removed from the guild @@ -321,19 +321,19 @@ class Guild extends Base { this.features = data.features || this.features || []; if (data.channels) { - this.channels.clear(); + this.channels.cache.clear(); for (const rawChannel of data.channels) { this.client.channels.add(rawChannel, this); } } if (data.roles) { - this.roles.clear(); + this.roles.cache.clear(); for (const role of data.roles) this.roles.add(role); } if (data.members) { - this.members.clear(); + this.members.cache.clear(); for (const guildUser of data.members) this.members.add(guildUser); } @@ -352,7 +352,7 @@ class Guild extends Base { } if (data.voice_states) { - this.voiceStates.clear(); + this.voiceStates.cache.clear(); for (const voiceState of data.voice_states) { this.voiceStates.add(voiceState); } @@ -360,10 +360,10 @@ class Guild extends Base { if (!this.emojis) { /** - * A collection of emojis that are in this guild. The key is the emoji's ID, the value is the emoji. - * @type {GuildEmojiStore} + * A manager of the emojis belonging to this guild + * @type {GuildEmojiManager} */ - this.emojis = new GuildEmojiStore(this); + this.emojis = new GuildEmojiManager(this); if (data.emojis) for (const emoji of data.emojis) this.emojis.add(emoji); } else if (data.emojis) { this.client.actions.GuildEmojisUpdate.handle({ @@ -463,7 +463,7 @@ class Guild extends Base { * @readonly */ get owner() { - return this.members.get(this.ownerID) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + return this.members.cache.get(this.ownerID) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? this.members.add({ user: { id: this.ownerID } }, true) : null); } @@ -474,7 +474,7 @@ class Guild extends Base { * @readonly */ get afkChannel() { - return this.client.channels.get(this.afkChannelID) || null; + return this.client.channels.cache.get(this.afkChannelID) || null; } /** @@ -483,7 +483,7 @@ class Guild extends Base { * @readonly */ get systemChannel() { - return this.client.channels.get(this.systemChannelID) || null; + return this.client.channels.cache.get(this.systemChannelID) || null; } /** @@ -492,7 +492,7 @@ class Guild extends Base { * @readonly */ get widgetChannel() { - return this.client.channels.get(this.widgetChannelID) || null; + return this.client.channels.cache.get(this.widgetChannelID) || null; } /** @@ -501,7 +501,7 @@ class Guild extends Base { * @readonly */ get embedChannel() { - return this.client.channels.get(this.embedChannelID) || null; + return this.client.channels.cache.get(this.embedChannelID) || null; } /** @@ -510,9 +510,10 @@ class Guild extends Base { * @readonly */ get me() { - return this.members.get(this.client.user.id) || (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? - this.members.add({ user: { id: this.client.user.id } }, true) : - null); + return this.members.cache.get(this.client.user.id) || + (this.client.options.partials.includes(PartialTypes.GUILD_MEMBER) ? + this.members.add({ user: { id: this.client.user.id } }, true) : + null); } /** @@ -521,7 +522,7 @@ class Guild extends Base { * @readonly */ get voice() { - return this.voiceStates.get(this.client.user.id); + return this.voiceStates.cache.get(this.client.user.id); } /** @@ -716,7 +717,7 @@ class Guild extends Base { fetchEmbed() { return this.client.api.guilds(this.id).embed.get().then(data => ({ enabled: data.enabled, - channel: data.channel_id ? this.channels.get(data.channel_id) : null, + channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null, })); } @@ -763,7 +764,7 @@ class Guild extends Base { addMember(user, options) { user = this.client.users.resolveID(user); if (!user) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable')); - if (this.members.has(user)) return Promise.resolve(this.members.get(user)); + if (this.members.cache.has(user)) return Promise.resolve(this.members.cache.get(user)); options.access_token = options.accessToken; if (options.roles) { const roles = []; @@ -988,7 +989,7 @@ class Guild extends Base { * @returns {Promise} * @example * // Edit the guild owner - * guild.setOwner(guild.members.first()) + * guild.setOwner(guild.members.cache.first()) * .then(updated => console.log(`Updated the guild owner to ${updated.owner.displayName}`)) * .catch(console.error); */ @@ -1203,7 +1204,7 @@ class Guild extends Base { * @private */ _sortedRoles() { - return Util.discordSort(this.roles); + return Util.discordSort(this.roles.cache); } /** @@ -1214,7 +1215,7 @@ class Guild extends Base { */ _sortedChannels(channel) { const category = channel.type === ChannelTypes.CATEGORY; - return Util.discordSort(this.channels.filter(c => + return Util.discordSort(this.channels.cache.filter(c => c.type === channel.type && (category || c.parent === channel.parent) )); } diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index d1185bdd9..a6327f35c 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -285,7 +285,7 @@ class GuildAuditLogsEntry { */ this.executor = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users.add({ id: data.user_id }) : - guild.client.users.get(data.user_id); + guild.client.users.cache.get(data.user_id); /** * An entry in the audit log representing a specific change. @@ -321,7 +321,7 @@ class GuildAuditLogsEntry { } else if (data.action_type === Actions.MESSAGE_DELETE) { this.extra = { count: data.options.count, - channel: guild.channels.get(data.options.channel_id), + channel: guild.channels.cache.get(data.options.channel_id), }; } else if (data.action_type === Actions.MESSAGE_BULK_DELETE) { this.extra = { @@ -330,11 +330,11 @@ class GuildAuditLogsEntry { } else { switch (data.options.type) { case 'member': - this.extra = guild.members.get(data.options.id); + this.extra = guild.members.cache.get(data.options.id); if (!this.extra) this.extra = { id: data.options.id }; break; case 'role': - this.extra = guild.roles.get(data.options.id); + this.extra = guild.roles.cache.get(data.options.id); if (!this.extra) this.extra = { id: data.options.id, name: data.options.role_name }; break; default: @@ -357,9 +357,9 @@ class GuildAuditLogsEntry { } else if (targetType === Targets.USER) { this.target = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users.add({ id: data.target_id }) : - guild.client.users.get(data.target_id); + guild.client.users.cache.get(data.target_id); } else if (targetType === Targets.GUILD) { - this.target = guild.client.guilds.get(data.target_id); + this.target = guild.client.guilds.cache.get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = logs.webhooks.get(data.target_id) || new Webhook(guild.client, @@ -386,9 +386,9 @@ class GuildAuditLogsEntry { } }); } else if (targetType === Targets.MESSAGE) { - this.target = guild.client.users.get(data.target_id); + this.target = guild.client.users.cache.get(data.target_id); } else { - this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id) || { id: data.target_id }; + this.target = guild[`${targetType.toLowerCase()}s`].cache.get(data.target_id) || { id: data.target_id }; } } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index fa1f84d43..649e5b222 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -72,7 +72,7 @@ class GuildChannel extends Channel { * @readonly */ get parent() { - return this.guild.channels.get(this.parentID) || null; + return this.guild.channels.cache.get(this.parentID) || null; } /** @@ -118,7 +118,7 @@ class GuildChannel extends Channel { if (!verified) member = this.guild.members.resolve(member); if (!member) return []; - roles = roles || member.roles; + roles = roles || member.roles.cache; const roleOverwrites = []; let memberOverwrites; let everyoneOverwrites; @@ -149,7 +149,7 @@ class GuildChannel extends Channel { memberPermissions(member) { if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); - const roles = member.roles; + const roles = member.roles.cache; const permissions = new Permissions(roles.map(role => role.permissions)); if (permissions.has(Permissions.FLAGS.ADMINISTRATOR)) return new Permissions(Permissions.ALL).freeze(); @@ -274,7 +274,7 @@ class GuildChannel extends Channel { */ get members() { const members = new Collection(); - for (const member of this.guild.members.values()) { + for (const member of this.guild.members.cache.values()) { if (this.permissionsFor(member).has('VIEW_CHANNEL', false)) { members.set(member.id, member); } diff --git a/src/structures/GuildEmoji.js b/src/structures/GuildEmoji.js index a7620911d..64138b86b 100644 --- a/src/structures/GuildEmoji.js +++ b/src/structures/GuildEmoji.js @@ -1,6 +1,6 @@ 'use strict'; -const GuildEmojiRoleStore = require('../stores/GuildEmojiRoleStore'); +const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager'); const Permissions = require('../util/Permissions'); const { Error } = require('../errors'); const Emoji = require('./Emoji'); @@ -79,12 +79,12 @@ class GuildEmoji extends Emoji { } /** - * A collection of roles this emoji is active for (empty if all), mapped by role ID - * @type {GuildEmojiRoleStore} + * A manager for roles this emoji is active for. + * @type {GuildEmojiRoleManager} * @readonly */ get roles() { - return new GuildEmojiRoleStore(this); + return new GuildEmojiRoleManager(this); } /** @@ -168,15 +168,15 @@ class GuildEmoji extends Emoji { other.name === this.name && other.managed === this.managed && other.requiresColons === this.requiresColons && - other.roles.size === this.roles.size && - other.roles.every(role => this.roles.has(role.id)) + other.roles.cache.size === this.roles.cache.size && + other.roles.cache.every(role => this.roles.cache.has(role.id)) ); } else { return ( other.id === this.id && other.name === this.name && - other.roles.length === this.roles.size && - other.roles.every(role => this.roles.has(role)) + other.roles.length === this.roles.cache.size && + other.roles.every(role => this.roles.cache.has(role)) ); } } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 8a8b8bb6a..b832f9593 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -3,7 +3,7 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Role = require('./Role'); const Permissions = require('../util/Permissions'); -const GuildMemberRoleStore = require('../stores/GuildMemberRoleStore'); +const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); const Base = require('./Base'); const VoiceState = require('./VoiceState'); const { Presence } = require('./Presence'); @@ -101,12 +101,12 @@ class GuildMember extends Base { } /** - * A collection of roles that are applied to this member, mapped by the role ID - * @type {GuildMemberRoleStore} + * A manager for the roles belonging to this member + * @type {GuildMemberRoleManager} * @readonly */ get roles() { - return new GuildMemberRoleStore(this); + return new GuildMemberRoleManager(this); } /** @@ -115,8 +115,8 @@ class GuildMember extends Base { * @readonly */ get lastMessage() { - const channel = this.guild.channels.get(this.lastMessageChannelID); - return (channel && channel.messages.get(this.lastMessageID)) || null; + const channel = this.guild.channels.cache.get(this.lastMessageChannelID); + return (channel && channel.messages.cache.get(this.lastMessageID)) || null; } /** @@ -125,7 +125,7 @@ class GuildMember extends Base { * @readonly */ get voice() { - return this.guild.voiceStates.get(this.id) || new VoiceState(this.guild, { user_id: this.id }); + return this.guild.voiceStates.cache.get(this.id) || new VoiceState(this.guild, { user_id: this.id }); } /** @@ -152,7 +152,7 @@ class GuildMember extends Base { * @readonly */ get presence() { - return this.guild.presences.get(this.id) || new Presence(this.client, { + return this.guild.presences.cache.get(this.id) || new Presence(this.client, { user: { id: this.id, }, @@ -205,7 +205,7 @@ class GuildMember extends Base { */ get permissions() { if (this.user.id === this.guild.ownerID) return new Permissions(Permissions.ALL).freeze(); - return new Permissions(this.roles.map(role => role.permissions)).freeze(); + return new Permissions(this.roles.cache.map(role => role.permissions)).freeze(); } /** @@ -261,7 +261,7 @@ class GuildMember extends Base { */ hasPermission(permission, { checkAdmin = true, checkOwner = true } = {}) { if (checkOwner && this.user.id === this.guild.ownerID) return true; - return this.roles.some(r => r.permissions.has(permission, checkAdmin)); + return this.roles.cache.some(r => r.permissions.has(permission, checkAdmin)); } /** diff --git a/src/structures/Integration.js b/src/structures/Integration.js index 5ff760dc7..8fef7aa22 100644 --- a/src/structures/Integration.js +++ b/src/structures/Integration.js @@ -56,7 +56,7 @@ class Integration extends Base { * The role that this integration uses for subscribers * @type {Role} */ - this.role = this.guild.roles.get(data.role_id); + this.role = this.guild.roles.cache.get(data.role_id); /** * The user for this integration diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 4d048b3f1..88b62b4a9 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -117,7 +117,7 @@ class Invite extends Base { */ get deletable() { const guild = this.guild; - if (!guild || !this.client.guilds.has(guild.id)) return false; + if (!guild || !this.client.guilds.cache.has(guild.id)) return false; if (!guild.me) throw new Error('GUILD_UNCACHED_ME'); return this.channel.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS, false) || guild.me.permissions.has(Permissions.FLAGS.MANAGE_GUILD); diff --git a/src/structures/Message.js b/src/structures/Message.js index b09f1d348..adc4ec834 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -7,7 +7,7 @@ const ReactionCollector = require('./ReactionCollector'); const ClientApplication = require('./ClientApplication'); const Util = require('../util/Util'); const Collection = require('../util/Collection'); -const ReactionStore = require('../stores/ReactionStore'); +const ReactionManager = require('../managers/ReactionManager'); const { MessageTypes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); const Base = require('./Base'); @@ -126,10 +126,10 @@ class Message extends Base { this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; /** - * A collection of reactions to this message, mapped by the reaction ID - * @type {ReactionStore} + * A manager of the reactions belonging to this message + * @type {ReactionManager} */ - this.reactions = new ReactionStore(this); + this.reactions = new ReactionManager(this); if (data.reactions && data.reactions.length > 0) { for (const reaction of data.reactions) { this.reactions.add(reaction); @@ -452,7 +452,7 @@ class Message extends Base { * .catch(console.error); * @example * // React to a message with a custom emoji - * message.react(message.guild.emojis.get('123456789012345678')) + * message.react(message.guild.emojis.cache.get('123456789012345678')) * .then(console.log) * .catch(console.error); */ @@ -484,7 +484,7 @@ class Message extends Base { */ delete({ timeout = 0, reason } = {}) { if (timeout <= 0) { - return this.channel.messages.remove(this.id, reason).then(() => this); + return this.channel.messages.delete(this.id, reason).then(() => this); } else { return new Promise(resolve => { this.client.setTimeout(() => { diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index ab2d5c81b..6079dc23b 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -71,7 +71,7 @@ class MessageMentions { } else { this.roles = new Collection(); for (const mention of roles) { - const role = message.channel.guild.roles.get(mention); + const role = message.channel.guild.roles.cache.get(mention); if (role) this.roles.set(role.id, role); } } @@ -156,7 +156,7 @@ class MessageMentions { this._channels = new Collection(); let matches; while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) { - const chan = this.client.channels.get(matches[1]); + const chan = this.client.channels.cache.get(matches[1]); if (chan) this._channels.set(chan.id, chan); } return this._channels; @@ -175,7 +175,7 @@ class MessageMentions { has(data, { ignoreDirect = false, ignoreRoles = false, ignoreEveryone = false } = {}) { if (!ignoreEveryone && this.everyone) return true; if (!ignoreRoles && data instanceof GuildMember) { - for (const role of this.roles.values()) if (data.roles.has(role.id)) return true; + for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true; } if (!ignoreDirect) { diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 5cb9210fb..150288a55 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -3,7 +3,7 @@ const GuildEmoji = require('./GuildEmoji'); const Util = require('../util/Util'); const ReactionEmoji = require('./ReactionEmoji'); -const ReactionUserStore = require('../stores/ReactionUserStore'); +const ReactionUserManager = require('../managers/ReactionUserManager'); /** * Represents a reaction to a message. @@ -35,10 +35,10 @@ class MessageReaction { this.me = data.me; /** - * The users that have given this reaction, mapped by their ID - * @type {ReactionUserStore} + * A manager of the users that have given this reaction + * @type {ReactionUserManager} */ - this.users = new ReactionUserStore(client, undefined, this); + this.users = new ReactionUserManager(client, undefined, this); this._emoji = new ReactionEmoji(this, data.emoji); @@ -75,7 +75,7 @@ class MessageReaction { if (this._emoji instanceof GuildEmoji) return this._emoji; // Check to see if the emoji has become known to the client if (this._emoji.id) { - const emojis = this.message.client.emojis; + const emojis = this.message.client.emojis.cache; if (emojis.has(this._emoji.id)) { const emoji = emojis.get(this._emoji.id); this._emoji = emoji; @@ -108,18 +108,18 @@ class MessageReaction { _add(user) { if (this.partial) return; - this.users.set(user.id, user); + this.users.cache.set(user.id, user); if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; if (!this.me) this.me = user.id === this.message.client.user.id; } _remove(user) { if (this.partial) return; - this.users.delete(user.id); + this.users.cache.delete(user.id); if (!this.me || user.id !== this.message.client.user.id) this.count--; if (user.id === this.message.client.user.id) this.me = false; - if (this.count <= 0 && this.users.size === 0) { - this.message.reactions.remove(this.emoji.id || this.emoji.name); + if (this.count <= 0 && this.users.cache.size === 0) { + this.message.reactions.cache.delete(this.emoji.id || this.emoji.name); } } } diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 12888f1c6..fdc038a5c 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -66,7 +66,7 @@ class Presence { * @readonly */ get user() { - return this.client.users.get(this.userID) || null; + return this.client.users.cache.get(this.userID) || null; } /** @@ -75,7 +75,7 @@ class Presence { * @readonly */ get member() { - return this.guild.members.get(this.userID) || null; + return this.guild.members.cache.get(this.userID) || null; } patch(data) { diff --git a/src/structures/ReactionCollector.js b/src/structures/ReactionCollector.js index e7d5aeb16..65b604cdf 100644 --- a/src/structures/ReactionCollector.js +++ b/src/structures/ReactionCollector.js @@ -74,7 +74,7 @@ class ReactionCollector extends Collector { this.on('remove', (reaction, user) => { this.total--; - if (!this.collected.some(r => r.users.has(user.id))) this.users.delete(user.id); + if (!this.collected.some(r => r.users.cache.has(user.id))) this.users.delete(user.id); }); } diff --git a/src/structures/Role.js b/src/structures/Role.js index 9c7ebd494..cedaa5456 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -117,7 +117,7 @@ class Role extends Base { * @readonly */ get members() { - return this.guild.members.filter(m => m.roles.has(this.id)); + return this.guild.members.cache.filter(m => m.roles.cache.has(this.id)); } /** diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 66212e70a..10b5d7cb3 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -5,7 +5,7 @@ const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); const DataResolver = require('../util/DataResolver'); -const MessageStore = require('../stores/MessageStore'); +const MessageManager = require('../managers/MessageManager'); /** * Represents a guild text channel on Discord. @@ -20,10 +20,10 @@ class TextChannel extends GuildChannel { constructor(guild, data) { super(guild, data); /** - * A collection containing the messages sent to this channel - * @type {MessageStore} + * A manager of the messages sent to this channel + * @type {MessageManager} */ - this.messages = new MessageStore(this); + this.messages = new MessageManager(this); this._typing = new Map(); } diff --git a/src/structures/User.js b/src/structures/User.js index 7ae67451b..ae3017052 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -119,8 +119,8 @@ class User extends Base { * @readonly */ get lastMessage() { - const channel = this.client.channels.get(this.lastMessageChannelID); - return (channel && channel.messages.get(this.lastMessageID)) || null; + const channel = this.client.channels.cache.get(this.lastMessageChannelID); + return (channel && channel.messages.cache.get(this.lastMessageID)) || null; } /** @@ -129,8 +129,8 @@ class User extends Base { * @readonly */ get presence() { - for (const guild of this.client.guilds.values()) { - if (guild.presences.has(this.id)) return guild.presences.get(this.id); + for (const guild of this.client.guilds.cache.values()) { + if (guild.presences.cache.has(this.id)) return guild.presences.cache.get(this.id); } return new Presence(this.client, { user: { id: this.id } }); } @@ -209,7 +209,7 @@ class User extends Base { * @readonly */ get dmChannel() { - return this.client.channels.find(c => c.type === 'dm' && c.recipient.id === this.id) || null; + return this.client.channels.cache.find(c => c.type === 'dm' && c.recipient.id === this.id) || null; } /** diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 15f2e496f..6b3850dfb 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -34,7 +34,7 @@ class VoiceChannel extends GuildChannel { */ get members() { const coll = new Collection(); - for (const state of this.guild.voiceStates.values()) { + for (const state of this.guild.voiceStates.cache.values()) { if (state.channelID === this.id && state.member) { coll.set(state.id, state.member); } diff --git a/src/structures/VoiceState.js b/src/structures/VoiceState.js index c60f6ac65..3ad02dea1 100644 --- a/src/structures/VoiceState.js +++ b/src/structures/VoiceState.js @@ -72,7 +72,7 @@ class VoiceState extends Base { * @readonly */ get member() { - return this.guild.members.get(this.id) || null; + return this.guild.members.cache.get(this.id) || null; } /** @@ -81,7 +81,7 @@ class VoiceState extends Base { * @readonly */ get channel() { - return this.guild.channels.get(this.channelID) || null; + return this.guild.channels.cache.get(this.channelID) || null; } /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 9b6b43c8f..b29607040 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -70,7 +70,7 @@ class Webhook { * The owner of the webhook * @type {?User|Object} */ - this.owner = this.client.users ? this.client.users.get(data.user.id) : data.user; + this.owner = this.client.users ? this.client.users.cache.get(data.user.id) : data.user; } else { this.owner = null; } @@ -154,7 +154,7 @@ class Webhook { query: { wait: true }, auth: false, }).then(d => { - const channel = this.client.channels ? this.client.channels.get(d.channel_id) : undefined; + const channel = this.client.channels ? this.client.channels.cache.get(d.channel_id) : undefined; if (!channel) return d; return channel.messages.add(d, false); }); diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index a6b183753..4106e00df 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -13,10 +13,10 @@ const APIMessage = require('../APIMessage'); class TextBasedChannel { constructor() { /** - * A collection containing the messages sent to this channel - * @type {MessageStore} + * A manager of the messages sent to this channel + * @type {MessageManager} */ - this.messages = new MessageStore(this); + this.messages = new MessageManager(this); /** * The ID of the last message in the channel, if one was sent @@ -37,7 +37,7 @@ class TextBasedChannel { * @readonly */ get lastMessage() { - return this.messages.get(this.lastMessageID) || null; + return this.messages.cache.get(this.lastMessageID) || null; } /** @@ -350,4 +350,4 @@ class TextBasedChannel { module.exports = TextBasedChannel; // Fixes Circular -const MessageStore = require('../../stores/MessageStore'); +const MessageManager = require('../../managers/MessageManager'); diff --git a/src/util/LimitedCollection.js b/src/util/LimitedCollection.js new file mode 100644 index 000000000..5719a9fa6 --- /dev/null +++ b/src/util/LimitedCollection.js @@ -0,0 +1,29 @@ +'use strict'; + +const Collection = require('./Collection.js'); + +/** + * A Collection which holds a max amount of entries. The first key is deleted if the Collection has + * reached max size. + * @extends {Collection} + * @param {number} [maxSize=0] The maximum size of the Collection + * @param {Iterable} [iterable=null] Optional entries passed to the Map constructor. + */ +class LimitedCollection extends Collection { + constructor(maxSize = 0, iterable = null) { + super(iterable); + /** + * The max size of the Collection. + * @type {number} + */ + this.maxSize = maxSize; + } + + set(key, value) { + if (this.maxSize === 0) return this; + if (this.size >= this.maxSize && !this.has(key)) this.delete(this.firstKey()); + return super.set(key, value); + } +} + +module.exports = LimitedCollection; diff --git a/src/util/Structures.js b/src/util/Structures.js index 02529d26d..c4061f62c 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -1,7 +1,7 @@ 'use strict'; /** - * Allows for the extension of built-in Discord.js structures that are instantiated by {@link DataStore DataStores}. + * Allows for the extension of built-in Discord.js structures that are instantiated by {@link BaseManager Managers}. */ class Structures { constructor() { diff --git a/src/util/Util.js b/src/util/Util.js index 6087f3c24..da4bff449 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -528,25 +528,25 @@ class Util { .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); if (message.channel.type === 'dm') { - const user = message.client.users.get(id); + const user = message.client.users.cache.get(id); return user ? `@${user.username}` : input; } - const member = message.channel.guild.members.get(id); + const member = message.channel.guild.members.cache.get(id); if (member) { return `@${member.displayName}`; } else { - const user = message.client.users.get(id); + const user = message.client.users.cache.get(id); return user ? `@${user.username}` : input; } }) .replace(/<#[0-9]+>/g, input => { - const channel = message.client.channels.get(input.replace(/<|#|>/g, '')); + const channel = message.client.channels.cache.get(input.replace(/<|#|>/g, '')); return channel ? `#${channel.name}` : input; }) .replace(/<@&[0-9]+>/g, input => { if (message.channel.type === 'dm') return input; - const role = message.guild.roles.get(input.replace(/<|@|>|&/g, '')); + const role = message.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; }); } @@ -571,34 +571,6 @@ class Util { setTimeout(resolve, ms); }); } - - /** - * Adds methods from collections and maps onto the provided store - * @param {DataStore} store The store to mixin - * @param {string[]} ignored The properties to ignore - * @private - */ - /* eslint-disable func-names */ - static mixin(store, ignored) { - const Collection = require('./Collection'); - Object.getOwnPropertyNames(Collection.prototype) - .concat(Object.getOwnPropertyNames(Map.prototype)).forEach(prop => { - if (ignored.includes(prop)) return; - if (prop === 'size') { - Object.defineProperty(store.prototype, prop, { - get: function() { - return this._filtered[prop]; - }, - }); - return; - } - const func = Collection.prototype[prop]; - if (prop === 'constructor' || typeof func !== 'function') return; - store.prototype[prop] = function(...args) { - return func.apply(this._filtered, args); - }; - }); - } } module.exports = Util; diff --git a/typings/index.d.ts b/typings/index.d.ts index 07f725bce..a0f40ce97 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -151,16 +151,16 @@ declare module 'discord.js' { private _eval(script: string): any; private _validateOptions(options?: ClientOptions): void; - public channels: ChannelStore; - public readonly emojis: GuildEmojiStore; - public guilds: GuildStore; + public channels: ChannelManager; + public readonly emojis: GuildEmojiManager; + public guilds: GuildManager; public readyAt: Date | null; public readonly readyTimestamp: number | null; public shard: ShardClientUtil | null; public token: string | null; public readonly uptime: number | null; public user: ClientUser | null; - public users: UserStore; + public users: UserManager; public voice: ClientVoiceManager | null; public ws: WebSocketManager; public destroy(): void; @@ -328,16 +328,6 @@ declare module 'discord.js' { public setUsername(username: string): Promise; } - export class Collection extends BaseCollection { - public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; - public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; - public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; - public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; - public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; - public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; - public toJSON(): object; - } - export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); private _timeout: NodeJS.Timer | null; @@ -648,7 +638,7 @@ declare module 'discord.js' { export class DMChannel extends TextBasedChannel(Channel) { constructor(client: Client, data?: object); - public messages: MessageStore; + public messages: MessageManager; public recipient: User; public readonly partial: false; public fetch(): Promise; @@ -680,7 +670,7 @@ declare module 'discord.js' { public applicationID: Snowflake; public available: boolean; public banner: string | null; - public channels: GuildChannelStore; + public channels: GuildChannelManager; public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; @@ -689,7 +679,7 @@ declare module 'discord.js' { public embedChannel: GuildChannel | null; public embedChannelID: Snowflake | null; public embedEnabled: boolean; - public emojis: GuildEmojiStore; + public emojis: GuildEmojiManager; public explicitContentFilter: number; public features: GuildFeatures[]; public icon: string | null; @@ -701,7 +691,7 @@ declare module 'discord.js' { public maximumPresences: number | null; public readonly me: GuildMember | null; public memberCount: number; - public members: GuildMemberStore; + public members: GuildMemberManager; public mfaLevel: number; public name: string; public readonly nameAcronym: string; @@ -710,9 +700,9 @@ declare module 'discord.js' { public readonly partnered: boolean; public premiumSubscriptionCount: number | null; public premiumTier: PremiumTier; - public presences: PresenceStore; + public presences: PresenceManager; public region: string; - public roles: RoleStore; + public roles: RoleManager; public readonly shard: WebSocketShard; public shardID: number; public splash: string | null; @@ -723,7 +713,7 @@ declare module 'discord.js' { public verificationLevel: number; public readonly verified: boolean; public readonly voice: VoiceState | null; - public readonly voiceStates: VoiceStateStore; + public readonly voiceStates: VoiceStateManager; public readonly widgetChannel: TextChannel | null; public widgetChannelID: Snowflake | null; public widgetEnabled: boolean | null; @@ -847,7 +837,7 @@ declare module 'discord.js' { public id: Snowflake; public managed: boolean; public requiresColons: boolean; - public roles: GuildEmojiRoleStore; + public roles: GuildEmojiRoleManager; public readonly url: string; public delete(reason?: string): Promise; public edit(data: GuildEmojiEditData, reason?: string): Promise; @@ -875,7 +865,7 @@ declare module 'discord.js' { public readonly premiumSince: Date | null; public premiumSinceTimestamp: number | null; public readonly presence: Presence; - public roles: GuildMemberRoleStore; + public roles: GuildMemberRoleManager; public user: User; public readonly voice: VoiceState; public ban(options?: BanOptions): Promise; @@ -978,7 +968,7 @@ declare module 'discord.js' { public readonly partial: false; public readonly pinnable: boolean; public pinned: boolean; - public reactions: ReactionStore; + public reactions: ReactionManager; public system: boolean; public tts: boolean; public type: MessageType; @@ -1113,7 +1103,7 @@ declare module 'discord.js' { public me: boolean; public message: Message; public readonly partial: boolean; - public users: ReactionUserStore; + public users: ReactionUserManager; public remove(): Promise; public fetch(): Promise; public toJSON(): object; @@ -1397,7 +1387,7 @@ declare module 'discord.js' { export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public messages: MessageStore; + public messages: MessageManager; public nsfw: boolean; public rateLimitPerUser: number; public topic: string | null; @@ -1409,7 +1399,7 @@ declare module 'discord.js' { export class NewsChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); - public messages: MessageStore; + public messages: MessageManager; public nsfw: boolean; public topic: string | null; public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; @@ -1749,64 +1739,64 @@ declare module 'discord.js' { //#endregion -//#region Stores +//#region Collections - export class ChannelStore extends DataStore { - constructor(client: Client, iterable: Iterable, options?: { lru: boolean }); - constructor(client: Client, options?: { lru: boolean }); - public fetch(id: Snowflake, cache?: boolean): Promise; - } - - export class DataStore, R = any> extends Collection { - constructor(client: Client, iterable: Iterable, holds: VConstructor); - public client: Client; - public holds: VConstructor; - public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): V; - public remove(key: K): void; - public resolve(resolvable: R): V | null; - public resolveID(resolvable: R): K | null; - // Don't worry about those bunch of ts-ignores here, this is intended https://github.com/microsoft/TypeScript/issues/1213 - // @ts-ignore - public filter(fn: (value: V, key: K, collection: this) => boolean): Collection; - // @ts-ignore - public filter(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): Collection; - // @ts-ignore - public filter(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): Collection; - // @ts-ignore - public partition(fn: (value: V, key: K, collection: this) => boolean): [Collection, Collection]; - // @ts-ignore - public partition(fn: (this: T, value: V, key: K, collection: this) => boolean, thisArg: T): [Collection, Collection]; - // @ts-ignore - public partition(fn: (value: V, key: K, collection: this) => boolean, thisArg?: unknown): [Collection, Collection]; + export class Collection extends BaseCollection { public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; - // @ts-ignore - public clone(): Collection; - // @ts-ignore - public concat(...collections: Collection[]): Collection; - // @ts-ignore - public sorted(compareFunction: (firstValue: V, secondValue: V, firstKey: K, secondKey: K) => number): Collection; + public toJSON(): object; } - export class GuildEmojiRoleStore extends OverridableDataStore { + export class LimitedCollection extends Collection { + public constructor(maxSize: number, iterable: Iterable); + public maxSize: number; + } + +//#endregion + +//#region Managers + + export class ChannelManager extends BaseManager { + constructor(client: Client, iterable: Iterable); + public fetch(id: Snowflake, cache?: boolean): Promise; + } + + export abstract class BaseManager { + constructor(client: Client, iterable: Iterable, holds: Constructable, cacheType: Collection); + public holds: Constructable; + public cache: Collection; + public cacheType: Collection; + public readonly client: Client; + public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): Holds; + public remove(key: K): void; + public resolve(resolvable: R): Holds | null; + public resolveID(resolvable: R): K | null; + } + + export class GuildEmojiRoleManager { constructor(emoji: GuildEmoji); + public emoji: GuildEmoji; + public guild: Guild; + public cache: Collection; public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; public set(roles: RoleResolvable[] | Collection): Promise; public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; } - export class GuildEmojiStore extends DataStore { + export class GuildEmojiManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; } - export class GuildChannelStore extends DataStore { + export class GuildChannelManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; public create(name: string, options: GuildCreateChannelOptions & { type: 'voice' }): Promise; public create(name: string, options: GuildCreateChannelOptions & { type: 'category' }): Promise; public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text' }): Promise; @@ -1814,78 +1804,87 @@ declare module 'discord.js' { } // Hacky workaround because changing the signature of an overridden method errors - class OverridableDataStore, R = any> extends DataStore { + class OverridableManager extends BaseManager { public add(data: any, cache: any): any; public set(key: any): any; } - export class GuildMemberRoleStore extends OverridableDataStore { + export class GuildMemberRoleManager extends OverridableManager { constructor(member: GuildMember); public readonly hoist: Role | null; public readonly color: Role | null; public readonly highest: Role; + public member: GuildMember; + public guild: Guild; public add(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; public set(roles: RoleResolvable[] | Collection, reason?: string): Promise; public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; } - export class GuildMemberStore extends DataStore { + export class GuildMemberManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; public ban(user: UserResolvable, options?: BanOptions): Promise; public fetch(options: UserResolvable | FetchMemberOptions): Promise; - public fetch(): Promise; + public fetch(): Promise; public fetch(options: FetchMembersOptions): Promise>; public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; } - export class GuildStore extends DataStore { + export class GuildManager extends BaseManager { constructor(client: Client, iterable?: Iterable); public create(name: string, options?: { region?: string, icon: BufferResolvable | Base64Resolvable | null }): Promise; } - export class MessageStore extends DataStore { + export class MessageManager extends BaseManager { constructor(channel: TextChannel | DMChannel, iterable?: Iterable); + public channel: TextBasedChannelFields; + public cache: LimitedCollection; public fetch(message: Snowflake, cache?: boolean): Promise; public fetch(options?: ChannelLogsQueryOptions, cache?: boolean): Promise>; public fetchPinned(cache?: boolean): Promise>; - public remove(message: MessageResolvable, reason?: string): Promise; + public delete(message: MessageResolvable, reason?: string): Promise; } - export class PresenceStore extends DataStore { + export class PresenceManager extends BaseManager { constructor(client: Client, iterable?: Iterable); } - export class ReactionStore extends DataStore { + export class ReactionManager extends BaseManager { constructor(message: Message, iterable?: Iterable); + public message: Message; public removeAll(): Promise; } - export class ReactionUserStore extends DataStore { + export class ReactionUserManager extends BaseManager { constructor(client: Client, iterable: Iterable | undefined, reaction: MessageReaction); + public reaction: MessageReaction; public fetch(options?: { limit?: number, after?: Snowflake, before?: Snowflake }): Promise>; public remove(user?: UserResolvable): Promise; } - export class RoleStore extends DataStore { + export class RoleManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); public readonly everyone: Role | null; public readonly highest: Role; + public guild: Guild; public create(options?: { data?: RoleData, reason?: string }): Promise; public fetch(id: Snowflake, cache?: boolean): Promise; public fetch(id?: Snowflake, cache?: boolean): Promise; } - export class UserStore extends DataStore { + export class UserManager extends BaseManager { constructor(client: Client, iterable?: Iterable); public fetch(id: Snowflake, cache?: boolean): Promise; } - export class VoiceStateStore extends DataStore { + export class VoiceStateManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; } //#endregion From 324d9e0a3af5d89dba35f6bc859f084b63adf104 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 17:24:35 +0000 Subject: [PATCH 1284/1359] fix(TextChannel): remove old nsfw regex check (#3775) --- src/structures/TextChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 10b5d7cb3..e77947c26 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -41,7 +41,7 @@ class TextChannel extends GuildChannel { * @type {boolean} * @readonly */ - this.nsfw = data.nsfw || /^nsfw(-|$)/.test(this.name); + this.nsfw = data.nsfw; /** * The ID of the last message sent in this channel, if one was sent From 92bc63452019f22161df68b1684b4b2b7e120a70 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Wed, 12 Feb 2020 18:28:26 +0100 Subject: [PATCH 1285/1359] docs(*Manager): fix child classes' cache type annotations (#3777) --- src/managers/ChannelManager.js | 5 ++--- src/managers/GuildChannelManager.js | 5 ++--- src/managers/GuildEmojiManager.js | 5 ++--- src/managers/GuildManager.js | 5 ++--- src/managers/GuildMemberManager.js | 5 ++--- src/managers/MessageManager.js | 5 ++--- src/managers/PresenceManager.js | 5 ++--- src/managers/ReactionManager.js | 5 ++--- src/managers/ReactionUserManager.js | 5 ++--- src/managers/RoleManager.js | 5 ++--- src/managers/UserManager.js | 5 ++--- src/managers/VoiceStateManager.js | 5 ++--- 12 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js index dcb098c13..b47667e54 100644 --- a/src/managers/ChannelManager.js +++ b/src/managers/ChannelManager.js @@ -14,9 +14,8 @@ class ChannelManager extends BaseManager { /** * The cache of Channels - * @property {Collection} cache - * @memberof ChannelManager - * @instance + * @type {Collection} + * @name ChannelManager#cache */ add(data, guild, cache = true) { diff --git a/src/managers/GuildChannelManager.js b/src/managers/GuildChannelManager.js index 147a0b207..6f89501c4 100644 --- a/src/managers/GuildChannelManager.js +++ b/src/managers/GuildChannelManager.js @@ -22,9 +22,8 @@ class GuildChannelManager extends BaseManager { /** * The cache of this Manager - * @property {Collection} cache - * @memberof GuildChannelManager - * @instance + * @type {Collection} + * @name GuildChannelManager#cache */ add(channel) { diff --git a/src/managers/GuildEmojiManager.js b/src/managers/GuildEmojiManager.js index 27ac363ca..85f030c30 100644 --- a/src/managers/GuildEmojiManager.js +++ b/src/managers/GuildEmojiManager.js @@ -23,9 +23,8 @@ class GuildEmojiManager extends BaseManager { /** * The cache of GuildEmojis - * @property {Collection} cache - * @memberof GuildEmojiManager - * @instance + * @type {Collection} + * @name GuildEmojiManager#cache */ add(data, cache) { diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index 5666156b5..c2747580e 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -19,9 +19,8 @@ class GuildManager extends BaseManager { /** * The cache of this Manager - * @property {Collection} cache - * @memberof GuildManager - * @instance + * @type {Collection} + * @name GuildManager#cache */ /** diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index b7d49f5c3..7fae74c8f 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -22,9 +22,8 @@ class GuildMemberManager extends BaseManager { /** * The cache of this Manager - * @property {Collection} cache - * @memberof GuildMemberManager - * @instance + * @type {Collection} + * @name GuildMemberManager#cache */ add(data, cache = true) { diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js index 19eb82b8c..fa2b0843e 100644 --- a/src/managers/MessageManager.js +++ b/src/managers/MessageManager.js @@ -21,9 +21,8 @@ class MessageManager extends BaseManager { /** * The cache of Messages - * @property {LimitedCollection} cache - * @memberof MessageManager - * @instance + * @type {LimitedCollection} + * @name MessageManager#cache */ add(data, cache) { diff --git a/src/managers/PresenceManager.js b/src/managers/PresenceManager.js index 0a38fd874..7627b0ece 100644 --- a/src/managers/PresenceManager.js +++ b/src/managers/PresenceManager.js @@ -14,9 +14,8 @@ class PresenceManager extends BaseManager { /** * The cache of Presences - * @property {Collection} cache - * @memberof PresenceManager - * @instance + * @type {Collection} + * @name PresenceManager#cache */ add(data, cache) { diff --git a/src/managers/ReactionManager.js b/src/managers/ReactionManager.js index 7350aebda..a9f84709c 100644 --- a/src/managers/ReactionManager.js +++ b/src/managers/ReactionManager.js @@ -24,9 +24,8 @@ class ReactionManager extends BaseManager { /** * The reaction cache of this manager - * @property {Collection} cache - * @memberof ReactionManager - * @instance + * @type {Collection} + * @name ReactionManager#cache */ /** diff --git a/src/managers/ReactionUserManager.js b/src/managers/ReactionUserManager.js index c82b119d1..feac5d635 100644 --- a/src/managers/ReactionUserManager.js +++ b/src/managers/ReactionUserManager.js @@ -20,9 +20,8 @@ class ReactionUserManager extends BaseManager { /** * The cache of this manager - * @property {Collection} cache - * @memberof GuildManager - * @instance + * @type {Collection} + * @name ReactionUserManager#cache */ /** diff --git a/src/managers/RoleManager.js b/src/managers/RoleManager.js index 9eb03cf2c..80f92ad11 100644 --- a/src/managers/RoleManager.js +++ b/src/managers/RoleManager.js @@ -21,9 +21,8 @@ class RoleManager extends BaseManager { /** * The role cache of this manager - * @property {Collection} cache - * @memberof RoleManager - * @instance + * @type {Collection} + * @name RoleManager#cache */ add(data, cache) { diff --git a/src/managers/UserManager.js b/src/managers/UserManager.js index 8818206aa..096520078 100644 --- a/src/managers/UserManager.js +++ b/src/managers/UserManager.js @@ -16,9 +16,8 @@ class UserManager extends BaseManager { /** * The cache of this manager - * @property {Collection} cache - * @memberof UserManager - * @instance + * @type {Collection} + * @name UserManager#cache */ /** diff --git a/src/managers/VoiceStateManager.js b/src/managers/VoiceStateManager.js index 755392b2d..4a26b3047 100644 --- a/src/managers/VoiceStateManager.js +++ b/src/managers/VoiceStateManager.js @@ -19,9 +19,8 @@ class VoiceStateManager extends BaseManager { /** * The cache of this manager - * @property {Collection} cache - * @memberof VoiceStateManager - * @instance + * @type {Collection} + * @name VoiceStateManager#cache */ add(data, cache = true) { From 94bb2686396ce97b50015b0fa34a6c27528afdbd Mon Sep 17 00:00:00 2001 From: Androz Date: Wed, 12 Feb 2020 18:36:08 +0100 Subject: [PATCH 1286/1359] docs: add extends to ChannelManager, cache is not nullable, resolveID accepts an object (#3771) * Add extends to docs * Cache shouldn't be nullable * jsdoc: use same type for both resolve methods --- src/managers/BaseManager.js | 4 ++-- src/managers/ChannelManager.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/managers/BaseManager.js b/src/managers/BaseManager.js index b9d0be938..ae07ee62f 100644 --- a/src/managers/BaseManager.js +++ b/src/managers/BaseManager.js @@ -35,7 +35,7 @@ class BaseManager { /** * Holds the cache for the data model - * @type {?Collection} + * @type {Collection} */ this.cache = new cacheType(...cacheOptions); if (iterable) for (const i of iterable) this.add(i); @@ -64,7 +64,7 @@ class BaseManager { /** * Resolves a data entry to a instance ID. - * @param {string|Instance} idOrInstance The id or instance of something in this Manager + * @param {string|Object} idOrInstance The id or instance of something in this Manager * @returns {?Snowflake} */ resolveID(idOrInstance) { diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js index b47667e54..e85330440 100644 --- a/src/managers/ChannelManager.js +++ b/src/managers/ChannelManager.js @@ -6,6 +6,7 @@ const { Events } = require('../util/Constants'); /** * A manager of channels belonging to a client + * @extends {BaseManager} */ class ChannelManager extends BaseManager { constructor(client, iterable) { From 149f72b50be3b8ffb134d20911b513cfea1aaaf5 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 17:40:20 +0000 Subject: [PATCH 1287/1359] typings(GuildMemberManager): fetch with no parameters returns collection (#3773) --- typings/index.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index a0f40ce97..09edcef51 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1827,8 +1827,7 @@ declare module 'discord.js' { public guild: Guild; public ban(user: UserResolvable, options?: BanOptions): Promise; public fetch(options: UserResolvable | FetchMemberOptions): Promise; - public fetch(): Promise; - public fetch(options: FetchMembersOptions): Promise>; + public fetch(options?: FetchMembersOptions): Promise>; public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; From 3f8ea38b3a4217e02e997058ec1562ed41fedcc7 Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Wed, 12 Feb 2020 16:23:06 -0500 Subject: [PATCH 1288/1359] docs: info tag for ActivityType regarding CUSTOM_STATUS (#3757) --- src/util/Constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index 0417d881c..b613021bf 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -423,6 +423,7 @@ exports.MessageTypes = [ ]; /** + * Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users * The type of an activity of a users presence, e.g. `PLAYING`. Here are the available types: * * PLAYING * * STREAMING From 592021df924344e44cdd38d39a6574c09fdbf7dd Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Feb 2020 08:25:14 +1100 Subject: [PATCH 1289/1359] feat(Message): throw a TypeError if delete is passed a non-object (#3772) --- src/structures/Message.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index adc4ec834..a7b55560c 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -482,7 +482,9 @@ class Message extends Base { * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) * .catch(console.error); */ - delete({ timeout = 0, reason } = {}) { + delete(options = {}) { + if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true); + const { timeout = 0, reason } = options; if (timeout <= 0) { return this.channel.messages.delete(this.id, reason).then(() => this); } else { From 62b227c2bd194c5a6ed90bf5ca5ff45ecf59bdde Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Feb 2020 08:26:17 +1100 Subject: [PATCH 1290/1359] fix(BaseManager): BaseManager#valueOf should return cache (#3776) * BaseManager#valueOf should return cache * Update Util#flatten to handle valueOf being a Collection * Update Util.js - typo Co-Authored-By: Amish Shah Co-authored-by: Amish Shah --- src/managers/BaseManager.js | 4 ++++ src/util/Util.js | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/managers/BaseManager.js b/src/managers/BaseManager.js index ae07ee62f..ea3717791 100644 --- a/src/managers/BaseManager.js +++ b/src/managers/BaseManager.js @@ -72,6 +72,10 @@ class BaseManager { if (typeof idOrInstance === 'string') return idOrInstance; return null; } + + valueOf() { + return this.cache; + } } module.exports = BaseManager; diff --git a/src/util/Util.js b/src/util/Util.js index da4bff449..4ab518773 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -36,8 +36,10 @@ class Util { const elemIsObj = isObject(element); const valueOf = elemIsObj && typeof element.valueOf === 'function' ? element.valueOf() : null; - // If it's a collection, make the array of keys + // If it's a Collection, make the array of keys if (element instanceof require('./Collection')) out[newProp] = Array.from(element.keys()); + // If the valueOf is a Collection, use its array of keys + else if (valueOf instanceof require('./Collection')) out[newProp] = Array.from(valueOf.keys()); // If it's an array, flatten each element else if (Array.isArray(element)) out[newProp] = element.map(e => Util.flatten(e)); // If it's an object with a primitive `valueOf`, use that value From d43692b0f2556f4aca6d9c8b9fea4d968d56ee71 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 21:29:16 +0000 Subject: [PATCH 1291/1359] docs(Guild): channels is a manager of channels (#3779) --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index d9bad7cef..8c1805792 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -41,7 +41,7 @@ class Guild extends Base { this.members = new GuildMemberManager(this); /** - * A manager of the members belonging to this guild + * A manager of the channels belonging to this guild * @type {GuildChannelManager} */ this.channels = new GuildChannelManager(this); From 562b5bfca78a9c50d05a5ec658cffda04ff88b5c Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 21:42:43 +0000 Subject: [PATCH 1292/1359] refactor(VoiceChannel): use Permissions.FLAGS in speakable (#3780) --- src/structures/VoiceChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 6b3850dfb..e8362878b 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -88,7 +88,7 @@ class VoiceChannel extends GuildChannel { * @readonly */ get speakable() { - return this.permissionsFor(this.client.user).has('SPEAK', false); + return this.permissionsFor(this.client.user).has(Permissions.FLAGS.SPEAK, false); } /** From 878cc050d4311a0d81f602ab37beca265cf4be03 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 12 Feb 2020 21:47:24 +0000 Subject: [PATCH 1293/1359] fix(Guild): use snake case when editing system_channel_flags (#3781) --- src/structures/Guild.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 8c1805792..ca462381f 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -839,7 +839,7 @@ class Guild extends Base { Number(data.defaultMessageNotifications); } if (typeof data.systemChannelFlags !== 'undefined') { - _data.systemChannelFlags = SystemChannelFlags.resolve(data.systemChannelFlags); + _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); } return this.client.api.guilds(this.id).patch({ data: _data, reason }) .then(newData => this.client.actions.GuildUpdate.handle(newData).updated); From a36f3869b3e09fa3875f751797eea9ce92958c79 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Thu, 13 Feb 2020 09:07:34 +1100 Subject: [PATCH 1294/1359] fix(Message): handle undefined/null content in cleanContent getter (#3778) * Handle undefined/null content in Message#cleanContent * Typings * Update typings/index.d.ts Co-Authored-By: SpaceEEC Co-authored-by: SpaceEEC --- src/structures/Message.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/Message.js b/src/structures/Message.js index a7b55560c..b32c874c1 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -301,7 +301,8 @@ class Message extends Base { * @readonly */ get cleanContent() { - return Util.cleanContent(this.content, this); + // eslint-disable-next-line eqeqeq + return this.content != null ? Util.cleanContent(this.content, this) : null; } /** From 6770c7c786a195f166af4f159965a6d3e5541377 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Thu, 13 Feb 2020 21:46:46 +0000 Subject: [PATCH 1295/1359] typings: add invite events to WSEventType and constants (#3782) * fix event typings * add ws events to jsdoc --- src/util/Constants.js | 2 ++ typings/index.d.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/util/Constants.js b/src/util/Constants.js index b613021bf..e97adafd1 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -319,6 +319,8 @@ exports.PartialTypes = keyMirror([ * * GUILD_CREATE * * GUILD_DELETE * * GUILD_UPDATE + * * INVITE_CREATE + * * INVITE_DELETE * * GUILD_MEMBER_ADD * * GUILD_MEMBER_REMOVE * * GUILD_MEMBER_UPDATE diff --git a/typings/index.d.ts b/typings/index.d.ts index 09edcef51..3f72f175a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -416,6 +416,8 @@ declare module 'discord.js' { GUILD_CREATE: 'guildCreate'; GUILD_DELETE: 'guildDelete'; GUILD_UPDATE: 'guildUpdate'; + INVITE_CREATE: 'inviteCreate'; + INVITE_DELETE: 'inviteDelete'; GUILD_UNAVAILABLE: 'guildUnavailable'; GUILD_MEMBER_ADD: 'guildMemberAdd'; GUILD_MEMBER_REMOVE: 'guildMemberRemove'; @@ -2688,6 +2690,8 @@ declare module 'discord.js' { | 'GUILD_CREATE' | 'GUILD_DELETE' | 'GUILD_UPDATE' + | 'INVITE_CREATE' + | 'INVITE_DELETE' | 'GUILD_MEMBER_ADD' | 'GUILD_MEMBER_REMOVE' | 'GUILD_MEMBER_UPDATE' From 21d37ed0cc71b227be0feadc4ffbcd522ca4f00e Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 13 Feb 2020 23:48:36 +0200 Subject: [PATCH 1296/1359] docs: clarify what zlib-sync does (#3785) * docs: Clarify what zlib-sync does * docs: Update docs site welcome * src/docs: Remove `ws.compress` from docs This is a deprecated parameter and you shouldn't use it unless you have zlib-sync installed, and even then, compression is automatically enabled * docs: Apply suggestion Co-Authored-By: Sugden <28943913+NotSugden@users.noreply.github.com> Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> --- README.md | 2 +- docs/general/welcome.md | 2 +- src/util/Constants.js | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3a2957981..c0e35754a 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Using opusscript is only recommended for development environments where @discord For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) diff --git a/docs/general/welcome.md b/docs/general/welcome.md index 936c01a48..ae90aa259 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -46,7 +46,7 @@ Using opusscript is only recommended for development environments where @discord For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers. ### Optional packages -- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for faster WebSocket data inflation (`npm install zlib-sync`) +- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) - [erlpack](https://github.com/discordapp/erlpack) for significantly faster WebSocket data (de)serialisation (`npm install discordapp/erlpack`) - One of the following packages can be installed for faster voice packet encryption and decryption: - [sodium](https://www.npmjs.com/package/sodium) (`npm install sodium`) diff --git a/src/util/Constants.js b/src/util/Constants.js index e97adafd1..c3934a3f7 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -61,8 +61,6 @@ exports.DefaultOptions = { * WebSocket options (these are left as snake_case to match the API) * @typedef {Object} WebsocketOptions * @property {number} [large_threshold=250] Number of members in a guild to be considered large - * @property {boolean} [compress=false] Whether to compress data sent on the connection - * (defaults to `false` for browsers) */ ws: { large_threshold: 250, From bc5e2950d065fd7c65259d050063299fc4468136 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sun, 16 Feb 2020 18:24:12 +0000 Subject: [PATCH 1297/1359] fix(ReactionManager): update message if partial (#3789) * update message after fetching if it is partial * suggested changes Co-Authored-By: SpaceEEC Co-authored-by: SpaceEEC --- src/managers/ReactionManager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/managers/ReactionManager.js b/src/managers/ReactionManager.js index a9f84709c..31a98fdfc 100644 --- a/src/managers/ReactionManager.js +++ b/src/managers/ReactionManager.js @@ -73,6 +73,7 @@ class ReactionManager extends BaseManager { const existing = this.cache.get(id); if (!this._partial(reactionEmoji)) return existing; const data = await this.client.api.channels(this.message.channel.id).messages(this.message.id).get(); + if (this.message.partial) this.message._patch(data); if (!data.reactions || !data.reactions.some(r => (r.emoji.id || r.emoji.name) === id)) { reactionEmoji.reaction._patch({ count: 0 }); this.message.reactions.cache.delete(id); From 46ee06b4245ee930698f880ae35c90b321382858 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 16 Feb 2020 20:36:10 +0200 Subject: [PATCH 1298/1359] feat(Message): add support for flag editing / embed suppression (#3674) --- src/structures/APIMessage.js | 19 +++++++++++++++++++ src/structures/Message.js | 17 +++++++++++++++++ typings/index.d.ts | 2 ++ 3 files changed, 38 insertions(+) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 6edd65785..d68ed0563 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -6,6 +6,7 @@ const MessageAttachment = require('./MessageAttachment'); const { browser } = require('../util/Constants'); const Util = require('../util/Util'); const { RangeError } = require('../errors'); +const MessageFlags = require('../util/MessageFlags'); /** * Represents a message to be sent to the API. @@ -63,6 +64,16 @@ class APIMessage { return this.target instanceof User || this.target instanceof GuildMember; } + /** + * Whether or not the target is a message + * @type {boolean} + * @readonly + */ + get isMessage() { + const Message = require('./Message'); + return this.target instanceof Message; + } + /** * Makes the content of this message. * @returns {?(string|string[])} @@ -126,6 +137,7 @@ class APIMessage { const content = this.makeContent(); const tts = Boolean(this.options.tts); + let nonce; if (typeof this.options.nonce !== 'undefined') { nonce = parseInt(this.options.nonce); @@ -149,6 +161,12 @@ class APIMessage { if (this.options.avatarURL) avatarURL = this.options.avatarURL; } + let flags; + if (this.isMessage) { + // eslint-disable-next-line eqeqeq + flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; + } + this.data = { content, tts, @@ -157,6 +175,7 @@ class APIMessage { embeds, username, avatar_url: avatarURL, + flags, }; return this; } diff --git a/src/structures/Message.js b/src/structures/Message.js index b32c874c1..0c856fdae 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -532,6 +532,23 @@ class Message extends Base { return this.client.fetchWebhook(this.webhookID); } + /** + * Suppresses or unsuppresses embeds on a message + * @param {boolean} [suppress=true] If the embeds should be suppressed or not + * @returns {Promise} + */ + suppressEmbeds(suppress = true) { + const flags = new MessageFlags(this.flags.bitfield); + + if (suppress) { + flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } else { + flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS); + } + + return this.edit({ flags }); + } + /** * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This diff --git a/typings/index.d.ts b/typings/index.d.ts index 3f72f175a..d9daf995e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -994,6 +994,7 @@ declare module 'discord.js' { public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; public reply(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; public reply(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + public suppressEmbeds(suppress?: boolean): Promise; public toJSON(): object; public toString(): string; public unpin(): Promise; @@ -2423,6 +2424,7 @@ declare module 'discord.js' { content?: string; embed?: MessageEmbedOptions | null; code?: string | boolean; + flags?: BitFieldResolvable; } interface MessageEmbedOptions { From e4e977f44779bc602e91a998e9338d7c401eff15 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 16 Feb 2020 20:41:37 +0200 Subject: [PATCH 1299/1359] src: update client options and shards value if fetching shard count (#3787) * src: Update client options and shards value if fetching shard count * src: Fix bug and remove more dead code --- src/client/websocket/WebSocketManager.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/client/websocket/WebSocketManager.js b/src/client/websocket/WebSocketManager.js index 02139f484..0403b52c7 100644 --- a/src/client/websocket/WebSocketManager.js +++ b/src/client/websocket/WebSocketManager.js @@ -18,7 +18,7 @@ const BeforeReadyWhitelist = [ WSEvents.GUILD_MEMBER_REMOVE, ]; -const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1); +const UNRECOVERABLE_CLOSE_CODES = Object.keys(WSCodes).slice(1).map(Number); const UNRESUMABLE_CLOSE_CODES = [1000, 4006, 4007]; /** @@ -153,24 +153,17 @@ class WebSocketManager extends EventEmitter { this.gateway = `${gatewayURL}/`; - const { shards } = this.client.options; + let { shards } = this.client.options; if (shards === 'auto') { this.debug(`Using the recommended shard count provided by Discord: ${recommendedShards}`); this.totalShards = this.client.options.shardCount = recommendedShards; - if (!this.client.options.shards.length) { - this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); - } + shards = this.client.options.shards = Array.from({ length: recommendedShards }, (_, i) => i); } - if (Array.isArray(shards)) { - this.totalShards = shards.length; - this.debug(`Spawning shards: ${shards.join(', ')}`); - this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); - } else { - this.debug(`Spawning ${this.totalShards} shards`); - this.shardQueue = new Set(Array.from({ length: this.totalShards }, (_, id) => new WebSocketShard(this, id))); - } + this.totalShards = shards.length; + this.debug(`Spawning shards: ${shards.join(', ')}`); + this.shardQueue = new Set(shards.map(id => new WebSocketShard(this, id))); await this._handleSessionLimit(remaining, reset_after); @@ -340,7 +333,7 @@ class WebSocketManager extends EventEmitter { this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`); this.destroyed = true; this.shardQueue.clear(); - for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false }); + for (const shard of this.shards.values()) shard.destroy({ closeCode: 1000, reset: true, emit: false, log: false }); } /** From c4c6ad4a63fcbc46fdf4e6f1ea01472e67c31839 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 17 Feb 2020 13:15:59 +0000 Subject: [PATCH 1300/1359] docs/typings(WSEvents): add missing, remove duplicated and userbot events (#3800) - Added `MESSAGE_REACTION_REMOVE_ALL` - Remove duplicated `VOICE_STATE_UPDATE` - Remove userbot `USER_SETTINGS_UPDATE` --- src/util/Constants.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index c3934a3f7..cd6f5f5a6 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -341,8 +341,8 @@ exports.PartialTypes = keyMirror([ * * MESSAGE_REACTION_ADD * * MESSAGE_REACTION_REMOVE * * MESSAGE_REACTION_REMOVE_ALL + * * MESSAGE_REACTION_REMOVE_EMOJI * * USER_UPDATE - * * USER_SETTINGS_UPDATE * * PRESENCE_UPDATE * * TYPING_START * * VOICE_STATE_UPDATE diff --git a/typings/index.d.ts b/typings/index.d.ts index d9daf995e..6acc486d8 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2716,9 +2716,9 @@ declare module 'discord.js' { | 'MESSAGE_REACTION_ADD' | 'MESSAGE_REACTION_REMOVE' | 'MESSAGE_REACTION_REMOVE_ALL' + | 'MESSAGE_REACTION_REMOVE_EMOJI' | 'USER_UPDATE' | 'PRESENCE_UPDATE' - | 'VOICE_STATE_UPDATE' | 'TYPING_START' | 'VOICE_STATE_UPDATE' | 'VOICE_SERVER_UPDATE' From f85230812ff6de4f1ba1c021c93fe308fb0685e6 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Wed, 19 Feb 2020 20:34:26 +0000 Subject: [PATCH 1301/1359] typings(Guild): mark afkChannel* & applicationID as nullable (#3805) --- typings/index.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 6acc486d8..4372b4ecf 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -666,10 +666,10 @@ declare module 'discord.js' { private _sortedChannels(channel: Channel): Collection; private _memberSpeakUpdate(user: Snowflake, speaking: boolean): void; - public readonly afkChannel: VoiceChannel; - public afkChannelID: Snowflake; + public readonly afkChannel: VoiceChannel | null; + public afkChannelID: Snowflake | null; public afkTimeout: number; - public applicationID: Snowflake; + public applicationID: Snowflake | null; public available: boolean; public banner: string | null; public channels: GuildChannelManager; From f6075a6e3ac31674b66546481dfeae62329dc6cc Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 21 Feb 2020 21:48:08 +0100 Subject: [PATCH 1302/1359] typings(Constants): add VerificationLevels (#3811) --- typings/index.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index 4372b4ecf..dcde300ee 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -509,6 +509,13 @@ declare module 'discord.js' { DARK_BUT_NOT_BLACK: 0x2C2F33; NOT_QUITE_BLACK: 0x23272A; }; + VerificationLevels: [ + 'None', + 'Low', + 'Medium', + '(╯°□°)╯︵ ┻━┻', + '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', + ]; Status: { READY: 0; CONNECTING: 1; From ef8acecc70542c0ffbebe4626c111c5dbd1f7993 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 22 Feb 2020 11:31:51 +0000 Subject: [PATCH 1303/1359] feat: add new MessageTypes (14 and 15) (#3812) * document types * typings * move comment --- src/util/Constants.js | 6 ++++++ typings/index.d.ts | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/util/Constants.js b/src/util/Constants.js index cd6f5f5a6..2bbe580d3 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -404,6 +404,8 @@ exports.WSEvents = keyMirror([ * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 * * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 * * CHANNEL_FOLLOW_ADD + * * GUILD_DISCOVERY_DISQUALIFIED + * * GUILD_DISCOVERY_REQUALIFIED * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -420,6 +422,10 @@ exports.MessageTypes = [ 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2', 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3', 'CHANNEL_FOLLOW_ADD', + // 13 isn't yet documented + null, + 'GUILD_DISCOVERY_DISQUALIFIED', + 'GUILD_DISCOVERY_REQUALIFIED', ]; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index dcde300ee..76a0dc93a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2479,7 +2479,9 @@ declare module 'discord.js' { | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2' | 'USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3' - | 'CHANNEL_FOLLOW_ADD'; + | 'CHANNEL_FOLLOW_ADD' + | 'GUILD_DISCOVERY_DISQUALIFIED' + | 'GUILD_DISCOVERY_REQUALIFIED'; interface OverwriteData { allow?: PermissionResolvable; From bea6da621d0e37000826c6fddf6f33c964704443 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 22 Feb 2020 12:04:41 +0000 Subject: [PATCH 1304/1359] feat(Guild): add rulesChannel and publicUpdatesChannel (#3810) * add rulesChannel* & publicUpdatesChannel* * update typings --- src/structures/Guild.js | 34 ++++++++++++++++++++++++++++++++++ typings/index.d.ts | 4 ++++ 2 files changed, 38 insertions(+) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index ca462381f..fc04530c9 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -320,6 +320,20 @@ class Guild extends Base { this.available = !data.unavailable; this.features = data.features || this.features || []; + /** + * The ID of the rules channel for the guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?Snowflake} + */ + this.rulesChannelID = data.rules_channel_id; + + /** + * The ID of the public updates channel for the guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?Snowflake} + */ + this.publicUpdatesChannelID = data.public_updates_channel_id; + if (data.channels) { this.channels.cache.clear(); for (const rawChannel of data.channels) { @@ -504,6 +518,26 @@ class Guild extends Base { return this.client.channels.cache.get(this.embedChannelID) || null; } + /** + * Rules channel for this guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?TextChannel} + * @readonly + */ + get rulesChannel() { + return this.client.channels.cache.get(this.rulesChannelID) || null; + } + + /** + * Public updates channel for this guild + * This is only available on guilds with the `PUBLIC` feature + * @type {?TextChannel} + * @readonly + */ + get publicUpdatesChannel() { + return this.client.channels.cache.get(this.publicUpdatesChannelID) || null; + } + /** * The client user as a GuildMember of this guild * @type {?GuildMember} diff --git a/typings/index.d.ts b/typings/index.d.ts index 76a0dc93a..373ff4055 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -710,8 +710,12 @@ declare module 'discord.js' { public premiumSubscriptionCount: number | null; public premiumTier: PremiumTier; public presences: PresenceManager; + public readonly publicUpdatesChannel: TextChannel | null; + public publicUpdatesChannelID: Snowflake | null; public region: string; public roles: RoleManager; + public readonly rulesChannel: TextChannel | null; + public rulesChannelID: Snowflake | null; public readonly shard: WebSocketShard; public shardID: number; public splash: string | null; From 161f90761aa357f17066f4171a5f43f76a552eb7 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 22 Feb 2020 12:25:27 +0000 Subject: [PATCH 1305/1359] feat(PartialGroupDMChannel): to support Invite#channel for group dms (#3786) * add PartialGroupDMChannel class * fix lint * add new errors * add new class to Channel.create * fix lint * update typings accordingly * better implement errors * remove unnecessary functions * oops * lint * lint * lint * more lint * more lint * jsdoc typo * suggested changes * i did not forget the typings --- src/errors/Messages.js | 3 ++ src/structures/Channel.js | 16 +++++++-- src/structures/PartialGroupDMChannel.js | 46 +++++++++++++++++++++++++ typings/index.d.ts | 7 ++++ 4 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/structures/PartialGroupDMChannel.js diff --git a/src/errors/Messages.js b/src/errors/Messages.js index db339ca2c..7d0556493 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -94,6 +94,9 @@ const Messages = { REACTION_RESOLVE_USER: 'Couldn\'t resolve the user ID to remove from the reaction.', VANITY_URL: 'This guild does not have the VANITY_URL feature enabled.', + + DELETE_GROUP_DM_CHANNEL: 'Bots don\'t have access to Group DM Channels and cannot delete them', + FETCH_GROUP_DM_CHANNEL: 'Bots don\'t have access to Group DM Channels and cannot fetch them', }; for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/structures/Channel.js b/src/structures/Channel.js index d5949c3cd..5e1194f8c 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -96,9 +96,19 @@ class Channel extends Base { static create(client, data, guild) { const Structures = require('../util/Structures'); let channel; - if (data.type === ChannelTypes.DM || (data.type !== ChannelTypes.GROUP && !data.guild_id && !guild)) { - const DMChannel = Structures.get('DMChannel'); - channel = new DMChannel(client, data); + if (!data.guild_id && !guild) { + switch (data.type) { + case ChannelTypes.DM: { + const DMChannel = Structures.get('DMChannel'); + channel = new DMChannel(client, data); + break; + } + case ChannelTypes.GROUP: { + const PartialGroupDMChannel = require('./PartialGroupDMChannel'); + channel = new PartialGroupDMChannel(client, data); + break; + } + } } else { guild = guild || client.guilds.cache.get(data.guild_id); if (guild) { diff --git a/src/structures/PartialGroupDMChannel.js b/src/structures/PartialGroupDMChannel.js new file mode 100644 index 000000000..e398f235a --- /dev/null +++ b/src/structures/PartialGroupDMChannel.js @@ -0,0 +1,46 @@ +'use strict'; + +const Channel = require('./Channel'); +const { Error } = require('../errors'); + +/** + * Represents a Partial Group DM Channel on Discord. + * @extends {Channel} + */ +class PartialGroupDMChannel extends Channel { + constructor(client, data) { + super(client, data); + + /** + * The name of this Group DM Channel + * @type {string} + */ + this.name = data.name; + + /** + * The hash of the channel icon + * @type {?string} + */ + this.icon = data.icon; + } + + /** + * The URL to this channel's icon. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {?string} + */ + iconURL({ format, size } = {}) { + if (!this.icon) return null; + return this.client.rest.cdn.GDMIcon(this.id, this.icon, format, size); + } + + delete() { + return Promise.reject(new Error('DELETE_GROUP_DM_CHANNEL')); + } + + fetch() { + return Promise.reject(new Error('FETCH_GROUP_DM_CHANNEL')); + } +} + +module.exports = PartialGroupDMChannel; diff --git a/typings/index.d.ts b/typings/index.d.ts index 373ff4055..f71b9da91 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -840,6 +840,13 @@ declare module 'discord.js' { public nsfw: boolean; } + export class PartialGroupDMChannel extends Channel { + constructor(client: Client, data: object); + public name: string; + public icon: string | null; + public iconURL(options?: ImageURLOptions): string | null; + } + export class GuildEmoji extends Emoji { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; From b347e9ec26c74e7b2996c92d57edf206d3b31c54 Mon Sep 17 00:00:00 2001 From: iBisho <37049432+iBisho@users.noreply.github.com> Date: Sat, 22 Feb 2020 18:36:29 -0300 Subject: [PATCH 1306/1359] refactor(MessageEmbed): simplify initialization of files property (#3814) --- src/structures/MessageEmbed.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 706b20e91..5ee0af07d 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -152,10 +152,7 @@ class MessageEmbed { * The files of this embed * @type {Array} */ - this.files = []; - if (data.files) { - this.files = data.files; - } + this.files = data.files || []; } /** From ecd8cccddf9b83f4f7cd858fdcad9e436ac51794 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sun, 23 Feb 2020 08:16:20 +0000 Subject: [PATCH 1307/1359] typings(AddGuildMemberOptions): change accessToken from String to string (#3815) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index f71b9da91..7ca76442e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2031,7 +2031,7 @@ declare module 'discord.js' { } interface AddGuildMemberOptions { - accessToken: String; + accessToken: string; nick?: string; roles?: Collection | RoleResolvable[]; mute?: boolean; From b727f6c1b9dfca28f77908c28071da423fb3e503 Mon Sep 17 00:00:00 2001 From: Souji Date: Sun, 23 Feb 2020 20:41:48 +0100 Subject: [PATCH 1308/1359] feat: bring embed builder field manipulation in line with underlying array functionality (#3761) * feat: splice multiple fields * remove MessageEmbed#spliceField * add MessageEmbed#spliceFields * to behave more like Array#splice * and allow multiple fields to be replaced/inserted * update typings accordingly * refactor: rename check to normalize * check suggests boolean return type * feat: allow spread args or array as field input * rewrite: replace addField in favor of addFields * typings: account for changes * chore: bump min node to 11.0.0 * for Array#flat * fix: bump min-node in package engines field * remove addBlankField --- README.md | 2 +- docs/general/faq.md | 2 +- docs/general/welcome.md | 2 +- package.json | 2 +- src/structures/MessageEmbed.js | 42 ++++++++++++++-------------------- typings/index.d.ts | 8 +++---- 6 files changed, 25 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index c0e35754a..b0b029697 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 10.2.0 or newer is required.** +**Node.js 11.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` diff --git a/docs/general/faq.md b/docs/general/faq.md index d57060dba..59616dfe8 100644 --- a/docs/general/faq.md +++ b/docs/general/faq.md @@ -3,7 +3,7 @@ These questions are some of the most frequently asked. ## No matter what, I get `SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode`‽ -Update to Node.js 10.0.0 or newer. +Update to Node.js 11.0.0 or newer. ## How do I get voice working? - Install FFMPEG. diff --git a/docs/general/welcome.md b/docs/general/welcome.md index ae90aa259..e2acee809 100644 --- a/docs/general/welcome.md +++ b/docs/general/welcome.md @@ -33,7 +33,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to - 100% coverage of the Discord API ## Installation -**Node.js 10.0.0 or newer is required.** +**Node.js 11.0.0 or newer is required.** Ignore any warnings about unmet peer dependencies, as they're all optional. Without voice support: `npm install discordjs/discord.js` diff --git a/package.json b/package.json index 230ec4709..b41d42c35 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "webpack-cli": "^3.2.3" }, "engines": { - "node": ">=10.2.0" + "node": ">=11.0.0" }, "browser": { "@discordjs/opus": false, diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 5ee0af07d..89664af1e 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -188,41 +188,24 @@ class MessageEmbed { } /** - * Adds a field to the embed (max 25). - * @param {StringResolvable} name The name of the field - * @param {StringResolvable} value The value of the field - * @param {boolean} [inline=false] Set the field to display inline + * Adds a fields to the embed (max 25). + * @param {...EmbedField|EmbedField[]} fields The fields to add * @returns {MessageEmbed} */ - addField(name, value, inline) { - this.fields.push(this.constructor.checkField(name, value, inline)); + addFields(...fields) { + this.fields.push(...this.constructor.normalizeFields(fields)); return this; } - /** - * Convenience function for `.addField('\u200B', '\u200B', inline)`. - * @param {boolean} [inline=false] Set the field to display inline - * @returns {MessageEmbed} - */ - addBlankField(inline) { - return this.addField('\u200B', '\u200B', inline); - } - /** * Removes, replaces, and inserts fields in the embed (max 25). * @param {number} index The index to start at * @param {number} deleteCount The number of fields to remove - * @param {StringResolvable} [name] The name of the field - * @param {StringResolvable} [value] The value of the field - * @param {boolean} [inline=false] Set the field to display inline + * @param {...EmbedField|EmbedField[]} [fields] The replacing field objects * @returns {MessageEmbed} */ - spliceField(index, deleteCount, name, value, inline) { - if (name && value) { - this.fields.splice(index, deleteCount, this.constructor.checkField(name, value, inline)); - } else { - this.fields.splice(index, deleteCount); - } + spliceFields(index, deleteCount, ...fields) { + this.fields.splice(index, deleteCount, ...this.constructor.normalizeFields(...fields)); return this; } @@ -373,13 +356,22 @@ class MessageEmbed { * @param {boolean} [inline=false] Set the field to display inline * @returns {EmbedField} */ - static checkField(name, value, inline = false) { + static normalizeField(name, value, inline = false) { name = Util.resolveString(name); if (!name) throw new RangeError('EMBED_FIELD_NAME'); value = Util.resolveString(value); if (!value) throw new RangeError('EMBED_FIELD_VALUE'); return { name, value, inline }; } + + /** + * Check for valid field input and resolves strings + * @param {...EmbedField|EmbedField[]} fields Fields to normalize + * @returns {EmbedField[]} + */ + static normalizeFields(...fields) { + return fields.flat(2).map(({ name, value, inline }) => this.normalizeField(name, value, inline)); + } } module.exports = MessageEmbed; diff --git a/typings/index.d.ts b/typings/index.d.ts index 7ca76442e..c15391eaf 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1070,8 +1070,7 @@ declare module 'discord.js' { public type: string; public url: string; public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number } | null; - public addBlankField(inline?: boolean): this; - public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; + public addFields(...fields: EmbedField[] | EmbedField[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; public setColor(color: ColorResolvable): this; @@ -1082,10 +1081,11 @@ declare module 'discord.js' { public setTimestamp(timestamp?: Date | number): this; public setTitle(title: StringResolvable): this; public setURL(url: string): this; - public spliceField(index: number, deleteCount: number, name?: StringResolvable, value?: StringResolvable, inline?: boolean): this; + public spliceFields(index: number, deleteCount: number, ...fields: EmbedField[] | EmbedField[][]): this; public toJSON(): object; - public static checkField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; + public static normalizeField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; + public static normalizeFields(...fields: EmbedField[] | EmbedField[][]): Required[]; } export class MessageMentions { From 4ec01ddef56272f6bed23dd0eced8ea9851127b7 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Sun, 23 Feb 2020 20:42:47 +0100 Subject: [PATCH 1309/1359] feat(MessageEmbed): change toJSON method to return an api-compatible object (#3813) --- src/structures/APIMessage.js | 2 +- src/structures/MessageEmbed.js | 9 ++------- typings/index.d.ts | 2 -- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index d68ed0563..b05fb68f9 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -152,7 +152,7 @@ class APIMessage { } else if (this.options.embed) { embedLikes.push(this.options.embed); } - const embeds = embedLikes.map(e => new MessageEmbed(e)._apiTransform()); + const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON()); let username; let avatarURL; diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 89664af1e..19a93c346 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -317,16 +317,11 @@ class MessageEmbed { return this; } - toJSON() { - return Util.flatten(this, { hexColor: true }); - } - /** - * Transforms the embed object to be processed. + * Transforms the embed to a plain object. * @returns {Object} The raw data of this embed - * @private */ - _apiTransform() { + toJSON() { return { title: this.title, type: 'rich', diff --git a/typings/index.d.ts b/typings/index.d.ts index c15391eaf..3a046059a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1051,8 +1051,6 @@ declare module 'discord.js' { export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); - private _apiTransform(): MessageEmbedOptions; - public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string } | null; public color: number; public readonly createdAt: Date | null; From 28490e84b069fcab660c9158d27931e9b013209f Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 09:41:29 +0000 Subject: [PATCH 1310/1359] typings(Invite): channel can be a PartialGroupDMChannel (#3823) --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 3a046059a..54a04853c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -931,7 +931,7 @@ declare module 'discord.js' { export class Invite extends Base { constructor(client: Client, data: object); - public channel: GuildChannel; + public channel: GuildChannel | PartialGroupDMChannel; public code: string; public readonly deletable: boolean; public readonly createdAt: Date | null; From e57ef25082eb3b2447b73b551dfc7cc0d4d59a19 Mon Sep 17 00:00:00 2001 From: Tenpi <37512637+Tenpi@users.noreply.github.com> Date: Mon, 24 Feb 2020 05:44:54 -0500 Subject: [PATCH 1311/1359] typings/fix(MessageEmbed): add interfaces for props, fix copy constructor (#3492) * updated typings * Updated docs * fixed types for MessageEmbedOptions * added curly bracket spaces * fix(MessageEmbed): make copy constructor work properly * fix(MessageEmbed): copy the provider too Co-authored-by: SpaceEEC --- src/structures/MessageEmbed.js | 61 +++++++++++++++++++++++--------- typings/index.d.ts | 63 +++++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 29 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 19a93c346..66f087220 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -67,81 +67,108 @@ class MessageEmbed { this.fields = data.fields ? data.fields.map(Util.cloneObject) : []; /** - * The thumbnail of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedThumbnail * @property {string} url URL for this thumbnail * @property {string} proxyURL ProxyURL for this thumbnail * @property {number} height Height of this thumbnail * @property {number} width Width of this thumbnail */ + + /** + * The thumbnail of this embed (if there is one) + * @type {?MessageEmbedThumbnail} + */ this.thumbnail = data.thumbnail ? { url: data.thumbnail.url, - proxyURL: data.thumbnail.proxy_url, + proxyURL: data.thumbnail.proxyURL || data.thumbnail.proxy_url, height: data.thumbnail.height, width: data.thumbnail.width, } : null; /** - * The image of this embed, if there is one - * @type {?Object} + * @typedef {Object} MessageEmbedImage * @property {string} url URL for this image * @property {string} proxyURL ProxyURL for this image * @property {number} height Height of this image * @property {number} width Width of this image */ + + /** + * The image of this embed, if there is one + * @type {?MessageEmbedImage} + */ this.image = data.image ? { url: data.image.url, - proxyURL: data.image.proxy_url, + proxyURL: data.image.proxyURL || data.image.proxy_url, height: data.image.height, width: data.image.width, } : null; /** - * The video of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedVideo * @property {string} url URL of this video * @property {string} proxyURL ProxyURL for this video * @property {number} height Height of this video * @property {number} width Width of this video + */ + + /** + * The video of this embed (if there is one) + * @type {?MessageEmbedVideo} * @readonly */ this.video = data.video ? { url: data.video.url, - proxyURL: data.video.proxy_url, + proxyURL: data.video.proxyURL || data.video.proxy_url, height: data.video.height, width: data.video.width, } : null; /** - * The author of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedAuthor * @property {string} name The name of this author * @property {string} url URL of this author * @property {string} iconURL URL of the icon for this author * @property {string} proxyIconURL Proxied URL of the icon for this author */ + + /** + * The author of this embed (if there is one) + * @type {?MessageEmbedAuthor} + */ this.author = data.author ? { name: data.author.name, url: data.author.url, iconURL: data.author.iconURL || data.author.icon_url, - proxyIconURL: data.author.proxyIconUrl || data.author.proxy_icon_url, + proxyIconURL: data.author.proxyIconURL || data.author.proxy_icon_url, } : null; /** - * The provider of this embed (if there is one) - * @type {?Object} + * @typedef {Object} MessageEmbedProvider * @property {string} name The name of this provider * @property {string} url URL of this provider */ - this.provider = data.provider; /** - * The footer of this embed - * @type {?Object} + * The provider of this embed (if there is one) + * @type {?MessageEmbedProvider} + */ + this.provider = data.provider ? { + name: data.provider.name, + url: data.provider.name, + } : null; + + /** + * @typedef {Object} MessageEmbedFooter * @property {string} text The text of this footer * @property {string} iconURL URL of the icon for this footer * @property {string} proxyIconURL Proxied URL of the icon for this footer */ + + /** + * The footer of this embed + * @type {?MessageEmbedFooter} + */ this.footer = data.footer ? { text: data.footer.text, iconURL: data.footer.iconURL || data.footer.icon_url, diff --git a/typings/index.d.ts b/typings/index.d.ts index 54a04853c..44851f008 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1051,23 +1051,23 @@ declare module 'discord.js' { export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); - public author: { name?: string; url?: string; iconURL?: string; proxyIconURL?: string } | null; + public author: MessageEmbedAuthor | null; public color: number; public readonly createdAt: Date | null; public description: string; public fields: EmbedField[]; public files: (MessageAttachment | string | FileOptions)[]; - public footer: { text?: string; iconURL?: string; proxyIconURL?: string } | null; + public footer: MessageEmbedFooter | null; public readonly hexColor: string | null; - public image: { url: string; proxyURL?: string; height?: number; width?: number; } | null; + public image: MessageEmbedImage | null; public readonly length: number; - public provider: { name: string; url: string; }; - public thumbnail: { url: string; proxyURL?: string; height?: number; width?: number; } | null; + public provider: MessageEmbedProvider | null; + public thumbnail: MessageEmbedThumbnail | null; public timestamp: number | null; public title: string; public type: string; public url: string; - public readonly video: { url?: string; proxyURL?: string; height?: number; width?: number } | null; + public readonly video: MessageEmbedVideo | null; public addFields(...fields: EmbedField[] | EmbedField[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; @@ -2449,13 +2449,52 @@ declare module 'discord.js' { url?: string; timestamp?: Date | number; color?: ColorResolvable; - fields?: { name: string; value: string; inline?: boolean; }[]; + fields?: EmbedField[]; files?: (MessageAttachment | string | FileOptions)[]; - author?: { name?: string; url?: string; icon_url?: string; iconURL?: string; }; - thumbnail?: { url?: string; height?: number; width?: number; }; - image?: { url?: string; proxy_url?: string; proxyURL?: string; height?: number; width?: number; }; - video?: { url?: string; height?: number; width?: number; }; - footer?: { text?: string; icon_url?: string; iconURL?: string; }; + author?: Partial & { icon_url?: string; proxy_icon_url?: string }; + thumbnail?: Partial & { proxy_url?: string }; + image?: Partial & { proxy_url?: string }; + video?: Partial & { proxy_url?: string }; + footer?: Partial & { icon_url?: string; proxy_icon_url?: string }; + } + + interface MessageEmbedAuthor { + name?: string; + url?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedThumbnail { + url: string; + proxyURL?: string; + height?: number; + width?: number; + } + + interface MessageEmbedFooter { + text?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedImage { + url: string; + proxyURL?: string; + height?: number; + width?: number; + } + + interface MessageEmbedProvider { + name: string; + url: string; + } + + interface MessageEmbedVideo { + url?: string; + proxyURL?: string; + height?: number; + width?: number; } interface MessageOptions { From ccb83a71ee5b85abaf6e90bc5ca3987c708aa57f Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 24 Feb 2020 13:03:02 +0100 Subject: [PATCH 1312/1359] docs/typings(MessageAttachment): mark spoiler as readonly, order spoiler in typings (#3714) --- src/structures/MessageAttachment.js | 1 + typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 1af5d1ed8..f5fb723b2 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -84,6 +84,7 @@ class MessageAttachment { /** * Whether or not this attachment has been marked as a spoiler * @type {boolean} + * @readonly */ get spoiler() { return Util.basename(this.url).startsWith('SPOILER_'); diff --git a/typings/index.d.ts b/typings/index.d.ts index 44851f008..69fb66d53 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1027,9 +1027,9 @@ declare module 'discord.js' { public name?: string; public proxyURL: string; public size: number; + public readonly spoiler: boolean; public url: string; public width: number | null; - public readonly spoiler: boolean; public setFile(attachment: BufferResolvable | Stream, name?: string): this; public setName(name: string): this; public toJSON(): object; From d406f42ce03f3e1cf1172dd0cfe4577c00ba514a Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 24 Feb 2020 13:03:45 +0100 Subject: [PATCH 1313/1359] docs/typings(SystemChannelFlags): properly document and use resolvable (#3794) - Change GuildEditData#systemChannelFlags to use SystemChannelFlagsResolvable - Move SystemChannelFlagsResolvable outside of class definition to make the docs generator happy --- src/util/SystemChannelFlags.js | 20 ++++++++++---------- typings/index.d.ts | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/util/SystemChannelFlags.js b/src/util/SystemChannelFlags.js index 14e8fd4f4..be953f71d 100644 --- a/src/util/SystemChannelFlags.js +++ b/src/util/SystemChannelFlags.js @@ -8,16 +8,16 @@ const BitField = require('./BitField'); * and by setting their corresponding flags you are disabling them * @extends {BitField} */ -class SystemChannelFlags extends BitField { - /** - * Data that can be resolved to give a sytem channel flag bitfield. This can be: - * * A string (see {@link SystemChannelFlags.FLAGS}) - * * A sytem channel flag - * * An instance of SystemChannelFlags - * * An Array of SystemChannelFlagsResolvable - * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable - */ -} +class SystemChannelFlags extends BitField {} + +/** + * Data that can be resolved to give a sytem channel flag bitfield. This can be: + * * A string (see {@link SystemChannelFlags.FLAGS}) + * * A sytem channel flag + * * An instance of SystemChannelFlags + * * An Array of SystemChannelFlagsResolvable + * @typedef {string|number|SystemChannelFlags|SystemChannelFlagsResolvable[]} SystemChannelFlagsResolvable + */ /** * Numeric system channel flags. All available properties: diff --git a/typings/index.d.ts b/typings/index.d.ts index 69fb66d53..8ed996454 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2334,7 +2334,7 @@ declare module 'discord.js' { defaultMessageNotifications?: DefaultMessageNotifications | number; afkChannel?: ChannelResolvable; systemChannel?: ChannelResolvable; - systemChannelFlags?: SystemChannelFlags; + systemChannelFlags?: SystemChannelFlagsResolvable; afkTimeout?: number; icon?: Base64Resolvable; owner?: GuildMemberResolvable; From 25cd23e305cee06e8bceaf133e770dd844dbdf50 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:06:00 +0000 Subject: [PATCH 1314/1359] cleanup(DMChannel): remove _cacheMessage (#3824) * cleanup(DMChannel): remove _cacheMessage * update actions/checkout to v2 Co-authored-by: SpaceEEC --- .github/workflows/test.yml | 6 +++--- src/structures/DMChannel.js | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c51d68e5..6bdfc2967 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node v12 uses: actions/setup-node@v1 @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node v12 uses: actions/setup-node@v1 @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Install Node v12 uses: actions/setup-node@v1 diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 9042519a9..e661bc44f 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -91,7 +91,6 @@ class DMChannel extends Channel { createMessageCollector() {} awaitMessages() {} // Doesn't work on DM channels; bulkDelete() {} - _cacheMessage() {} } TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']); From 967b533e9d5f08e2be50698fb98d38820d29d88b Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:06:22 +0000 Subject: [PATCH 1315/1359] typings(MessageEmbed): properly mark properties (#3822) Co-authored-by: SpaceEEC --- typings/index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 8ed996454..d3c1d5b8c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1052,9 +1052,9 @@ declare module 'discord.js' { export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); public author: MessageEmbedAuthor | null; - public color: number; + public color?: number; public readonly createdAt: Date | null; - public description: string; + public description?: string; public fields: EmbedField[]; public files: (MessageAttachment | string | FileOptions)[]; public footer: MessageEmbedFooter | null; @@ -1064,9 +1064,9 @@ declare module 'discord.js' { public provider: MessageEmbedProvider | null; public thumbnail: MessageEmbedThumbnail | null; public timestamp: number | null; - public title: string; + public title?: string; public type: string; - public url: string; + public url?: string; public readonly video: MessageEmbedVideo | null; public addFields(...fields: EmbedField[] | EmbedField[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; From 54f24d1fea7ea78672a61a4b1ac7af5efd0d19b0 Mon Sep 17 00:00:00 2001 From: Souji Date: Mon, 24 Feb 2020 14:02:06 +0100 Subject: [PATCH 1316/1359] typings(MessageEmebd): fix typings for addFields (#3821) * typings(MessageEmebd): fix typings for addFields * fix: add missing semicolon * docs(MessageEmbed): fix various types * in accordance with the scope of the PR * Update src/structures/MessageEmbed.js Co-Authored-By: SpaceEEC Co-authored-by: SpaceEEC --- src/structures/MessageEmbed.js | 13 ++++++++++--- typings/index.d.ts | 14 ++++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 66f087220..7cfb6d1aa 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -216,7 +216,7 @@ class MessageEmbed { /** * Adds a fields to the embed (max 25). - * @param {...EmbedField|EmbedField[]} fields The fields to add + * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add * @returns {MessageEmbed} */ addFields(...fields) { @@ -228,7 +228,7 @@ class MessageEmbed { * Removes, replaces, and inserts fields in the embed (max 25). * @param {number} index The index to start at * @param {number} deleteCount The number of fields to remove - * @param {...EmbedField|EmbedField[]} [fields] The replacing field objects + * @param {...EmbedFieldData|EmbedFieldData[]} [fields] The replacing field objects * @returns {MessageEmbed} */ spliceFields(index, deleteCount, ...fields) { @@ -386,9 +386,16 @@ class MessageEmbed { return { name, value, inline }; } + /** + * @typedef {Object} EmbedFieldData + * @property {StringResolvable} name The name of this field + * @property {StringResolvable} value The value of this field + * @property {boolean} [inline] If this field will be displayed inline + */ + /** * Check for valid field input and resolves strings - * @param {...EmbedField|EmbedField[]} fields Fields to normalize + * @param {...EmbedFieldData|EmbedFieldData[]} fields Fields to normalize * @returns {EmbedField[]} */ static normalizeFields(...fields) { diff --git a/typings/index.d.ts b/typings/index.d.ts index d3c1d5b8c..2bff7f52f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1068,7 +1068,7 @@ declare module 'discord.js' { public type: string; public url?: string; public readonly video: MessageEmbedVideo | null; - public addFields(...fields: EmbedField[] | EmbedField[][]): this; + public addFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; public setColor(color: ColorResolvable): this; @@ -1079,11 +1079,11 @@ declare module 'discord.js' { public setTimestamp(timestamp?: Date | number): this; public setTitle(title: StringResolvable): this; public setURL(url: string): this; - public spliceFields(index: number, deleteCount: number, ...fields: EmbedField[] | EmbedField[][]): this; + public spliceFields(index: number, deleteCount: number, ...fields: EmbedFieldData[] | EmbedFieldData[][]): this; public toJSON(): object; - public static normalizeField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; - public static normalizeFields(...fields: EmbedField[] | EmbedField[][]): Required[]; + public static normalizeField(name: StringResolvable, value: StringResolvable, inline?: boolean): Required; + public static normalizeFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): Required[]; } export class MessageMentions { @@ -2184,6 +2184,12 @@ declare module 'discord.js' { interface EmbedField { name: string; value: string; + inline: boolean; + } + + interface EmbedFieldData { + name: StringResolvable; + value: StringResolvable; inline?: boolean; } From e6d22527bb62b04581614d4dbd3de166471b749c Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 17:22:16 +0100 Subject: [PATCH 1317/1359] chore(typings): semicolon consistency (#3826) --- typings/index.d.ts | 118 ++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 2bff7f52f..a924fb4fd 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -83,7 +83,7 @@ declare module 'discord.js' { export class Base { constructor(client: Client); public readonly client: Client; - public toJSON(...props: { [key: string]: boolean | string }[]): object; + public toJSON(...props: { [key: string]: boolean | string; }[]): object; public valueOf(): string; } @@ -103,7 +103,7 @@ declare module 'discord.js' { public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; public setImmediate(fn: Function, ...args: any[]): NodeJS.Immediate; - public toJSON(...props: { [key: string]: boolean | string }[]): object; + public toJSON(...props: { [key: string]: boolean | string; }[]): object; } class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) { @@ -373,14 +373,14 @@ declare module 'discord.js' { types: PathLike; homepage: string; keywords: string[]; - bugs: { url: string }; - repository: { type: string, url: string }; - browser: { [key: string]: boolean }; - scripts: { [key: string]: string }; - engines: { [key: string]: string }; - dependencies: { [key: string]: string }; - peerDependencies: { [key: string]: string }; - devDependencies: { [key: string]: string }; + bugs: { url: string; }; + repository: { type: string, url: string; }; + browser: { [key: string]: boolean; }; + scripts: { [key: string]: string; }; + engines: { [key: string]: string; }; + dependencies: { [key: string]: string; }; + peerDependencies: { [key: string]: string; }; + devDependencies: { [key: string]: string; }; [key: string]: any; }; browser: boolean; @@ -738,15 +738,15 @@ declare module 'discord.js' { public equals(guild: Guild): boolean; public fetch(): Promise; public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise; - public fetchBan(user: UserResolvable): Promise<{ user: User, reason: string }>; - public fetchBans(): Promise>; + public fetchBan(user: UserResolvable): Promise<{ user: User; reason: string; }>; + public fetchBans(): Promise>; public fetchEmbed(): Promise; public fetchIntegrations(): Promise>; public fetchInvites(): Promise>; public fetchVanityCode(): Promise; public fetchVoiceRegions(): Promise>; public fetchWebhooks(): Promise>; - public iconURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; + public iconURL(options?: ImageURLOptions & { dynamic?: boolean; }): string | null; public leave(): Promise; public member(user: UserResolvable): GuildMember | null; public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise; @@ -826,11 +826,11 @@ declare module 'discord.js' { public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; public lockPermissions(): Promise; - public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string }): Promise; + public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string; }): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; public setName(name: string, reason?: string): Promise; - public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean, reason?: string }): Promise; - public setPosition(position: number, options?: { relative?: boolean, reason?: string }): Promise; + public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean; reason?: string; }): Promise; + public setPosition(position: number, options?: { relative?: boolean; reason?: string; }): Promise; public setTopic(topic: string, reason?: string): Promise; public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } @@ -893,7 +893,7 @@ declare module 'discord.js' { public createDM(): Promise; public deleteDM(): Promise; public edit(data: GuildMemberEditData, reason?: string): Promise; - public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean }): boolean; + public hasPermission(permission: PermissionResolvable, options?: { checkAdmin?: boolean; checkOwner?: boolean; }): boolean; public kick(reason?: string): Promise; public permissionsIn(channel: ChannelResolvable): Readonly; public setNickname(nickname: string, reason?: string): Promise; @@ -998,7 +998,7 @@ declare module 'discord.js' { public reference: MessageReference | null; public awaitReactions(filter: CollectorFilter, options?: AwaitReactionsOptions): Promise>; public createReactionCollector(filter: CollectorFilter, options?: ReactionCollectorOptions): ReactionCollector; - public delete(options?: { timeout?: number, reason?: string }): Promise; + public delete(options?: { timeout?: number; reason?: string; }): Promise; public edit(content: StringResolvable, options?: MessageEditOptions | MessageEmbed): Promise; public edit(options: MessageEditOptions | MessageEmbed | APIMessage): Promise; public equals(message: Message, rawData: object): boolean; @@ -1007,11 +1007,11 @@ declare module 'discord.js' { public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - public reply(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; - public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - public reply(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - public reply(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; public suppressEmbeds(suppress?: boolean): Promise; public toJSON(): object; public toString(): string; @@ -1138,7 +1138,7 @@ declare module 'discord.js' { public update(options: PermissionOverwriteOption, reason?: string): Promise; public delete(reason?: string): Promise; public toJSON(): object; - public static resolveOverwriteOptions(options: ResolvedOverwriteOptions, initialPermissions: { allow?: PermissionResolvable, deny?: PermissionResolvable }): ResolvedOverwriteOptions; + public static resolveOverwriteOptions(options: ResolvedOverwriteOptions, initialPermissions: { allow?: PermissionResolvable; deny?: PermissionResolvable; }): ResolvedOverwriteOptions; public static resolve(overwrite: OverwriteResolvable, guild: Guild): RawOverwriteData; } @@ -1242,7 +1242,7 @@ declare module 'discord.js' { public setMentionable(mentionable: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setPermissions(permissions: PermissionResolvable, reason?: string): Promise; - public setPosition(position: number, options?: { relative?: boolean; reason?: string }): Promise; + public setPosition(position: number, options?: { relative?: boolean; reason?: string; }): Promise; public toJSON(): object; public toString(): string; @@ -1410,7 +1410,7 @@ declare module 'discord.js' { public nsfw: boolean; public rateLimitPerUser: number; public topic: string | null; - public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public setRateLimitPerUser(rateLimitPerUser: number, reason?: string): Promise; public fetchWebhooks(): Promise>; @@ -1421,7 +1421,7 @@ declare module 'discord.js' { public messages: MessageManager; public nsfw: boolean; public topic: string | null; - public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable, reason?: string }): Promise; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; public setNSFW(nsfw: boolean, reason?: string): Promise; public fetchWebhooks(): Promise>; } @@ -1442,10 +1442,10 @@ declare module 'discord.js' { public system?: boolean; public readonly tag: string; public username: string; - public avatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string | null; + public avatarURL(options?: ImageURLOptions & { dynamic?: boolean; }): string | null; public createDM(): Promise; public deleteDM(): Promise; - public displayAvatarURL(options?: ImageURLOptions & { dynamic?: boolean }): string; + public displayAvatarURL(options?: ImageURLOptions & { dynamic?: boolean; }): string; public equals(user: User): boolean; public fetch(): Promise; public toString(): string; @@ -1462,7 +1462,7 @@ declare module 'discord.js' { public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; public static discordSort(collection: Collection): Collection; - public static escapeMarkdown(text: string, options?: { codeBlock?: boolean, inlineCode?: boolean, bold?: boolean, italic?: boolean, underline?: boolean, strikethrough?: boolean, spoiler?: boolean, inlineCodeContent?: boolean, codeBlockContent?: boolean }): string; + public static escapeMarkdown(text: string, options?: { codeBlock?: boolean; inlineCode?: boolean; bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; spoiler?: boolean; inlineCodeContent?: boolean; codeBlockContent?: boolean; }): string; public static escapeCodeBlock(text: string): string; public static escapeInlineCode(text: string): string; public static escapeBold(text: string): string; @@ -1472,10 +1472,10 @@ declare module 'discord.js' { public static escapeSpoiler(text: string): string; public static cleanCodeBlockContent(text: string): string; public static fetchRecommendedShards(token: string, guildsPerShard?: number): Promise; - public static flatten(obj: object, ...props: { [key: string]: boolean | string }[]): object; + public static flatten(obj: object, ...props: { [key: string]: boolean | string; }[]): object; public static idToBinary(num: Snowflake): string; - public static makeError(obj: { name: string, message: string, stack: string }): Error; - public static makePlainError(err: Error): { name: string, message: string, stack: string }; + public static makeError(obj: { name: string; message: string; stack: string; }): Error; + public static makePlainError(err: Error): { name: string; message: string; stack: string; }; public static mergeDefault(def: object, given: object): object; public static moveElementInArray(array: any[], element: any, newIndex: number, offset?: boolean): number; public static parseEmoji(text: string): { animated: boolean; name: string; id: string | null; } | null; @@ -1488,7 +1488,7 @@ declare module 'discord.js' { sorted: Collection, route: object, reason?: string - ): Promise<{ id: Snowflake; position: number }[]>; + ): Promise<{ id: Snowflake; position: number; }[]>; public static splitMessage(text: StringResolvable, options?: SplitOptions): string[]; public static str2ab(str: string): ArrayBuffer; } @@ -1592,7 +1592,7 @@ declare module 'discord.js' { class VoiceReceiver extends EventEmitter { constructor(connection: VoiceConnection); - public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm', end?: 'silence' | 'manual' }): Readable; + public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual'; }): Readable; public on(event: 'debug', listener: (error: Error | string) => void): this; public on(event: string, listener: Function): this; @@ -1639,7 +1639,7 @@ declare module 'discord.js' { } class VolumeInterface extends EventEmitter { - constructor(options?: { volume?: number }) + constructor(options?: { volume?: number; }); public readonly volume: number; public readonly volumeDecibels: number; public readonly volumeEditable: boolean; @@ -1736,7 +1736,7 @@ declare module 'discord.js' { private identifyResume(): void; private _send(data: object): void; private processQueue(): void; - private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean }): void; + private destroy(destroyOptions?: { closeCode?: number; reset?: boolean; emit?: boolean; log?: boolean; }): void; private _cleanupConnection(): void; private _emitDestroyed(): void; @@ -1790,7 +1790,7 @@ declare module 'discord.js' { public cache: Collection; public cacheType: Collection; public readonly client: Client; - public add(data: any, cache?: boolean, { id, extras }?: { id: K, extras: any[] }): Holds; + public add(data: any, cache?: boolean, { id, extras }?: { id: K; extras: any[]; }): Holds; public remove(key: K): void; public resolve(resolvable: R): Holds | null; public resolveID(resolvable: R): K | null; @@ -1816,9 +1816,9 @@ declare module 'discord.js' { export class GuildChannelManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; - public create(name: string, options: GuildCreateChannelOptions & { type: 'voice' }): Promise; - public create(name: string, options: GuildCreateChannelOptions & { type: 'category' }): Promise; - public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text' }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'voice'; }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'category'; }): Promise; + public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text'; }): Promise; public create(name: string, options: GuildCreateChannelOptions): Promise; } @@ -1847,14 +1847,14 @@ declare module 'discord.js' { public ban(user: UserResolvable, options?: BanOptions): Promise; public fetch(options: UserResolvable | FetchMemberOptions): Promise; public fetch(options?: FetchMembersOptions): Promise>; - public prune(options: GuildPruneMembersOptions & { dry?: false, count: false }): Promise; + public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; public unban(user: UserResolvable, reason?: string): Promise; } export class GuildManager extends BaseManager { constructor(client: Client, iterable?: Iterable); - public create(name: string, options?: { region?: string, icon: BufferResolvable | Base64Resolvable | null }): Promise; + public create(name: string, options?: { region?: string; icon: BufferResolvable | Base64Resolvable | null; }): Promise; } export class MessageManager extends BaseManager { @@ -1880,7 +1880,7 @@ declare module 'discord.js' { export class ReactionUserManager extends BaseManager { constructor(client: Client, iterable: Iterable | undefined, reaction: MessageReaction); public reaction: MessageReaction; - public fetch(options?: { limit?: number, after?: Snowflake, before?: Snowflake }): Promise>; + public fetch(options?: { limit?: number; after?: Snowflake; before?: Snowflake; }): Promise>; public remove(user?: UserResolvable): Promise; } @@ -1890,7 +1890,7 @@ declare module 'discord.js' { public readonly highest: Role; public guild: Guild; - public create(options?: { data?: RoleData, reason?: string }): Promise; + public create(options?: { data?: RoleData; reason?: string; }): Promise; public fetch(id: Snowflake, cache?: boolean): Promise; public fetch(id?: Snowflake, cache?: boolean): Promise; } @@ -1924,11 +1924,11 @@ declare module 'discord.js' { lastPinTimestamp: number | null; readonly lastPinAt: Date; send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - send(content?: StringResolvable, options?: MessageOptions & { split?: false } | MessageAdditions): Promise; - send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; } interface TextBasedChannelFields extends PartialTextBasedChannelFields { @@ -1951,10 +1951,10 @@ declare module 'discord.js' { readonly url: string; delete(reason?: string): Promise; edit(options: WebhookEditData): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false } | MessageAdditions): Promise; - send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions): Promise; - send(options?: WebhookMessageOptions & { split?: false } | MessageAdditions | APIMessage): Promise; - send(options?: WebhookMessageOptions & { split: true | SplitOptions } | MessageAdditions | APIMessage): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split?: false; } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: WebhookMessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; + send(options?: WebhookMessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + send(options?: WebhookMessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; sendSlackMessage(body: object): Promise; } @@ -2457,11 +2457,11 @@ declare module 'discord.js' { color?: ColorResolvable; fields?: EmbedField[]; files?: (MessageAttachment | string | FileOptions)[]; - author?: Partial & { icon_url?: string; proxy_icon_url?: string }; - thumbnail?: Partial & { proxy_url?: string }; - image?: Partial & { proxy_url?: string }; - video?: Partial & { proxy_url?: string }; - footer?: Partial & { icon_url?: string; proxy_icon_url?: string }; + author?: Partial & { icon_url?: string; proxy_icon_url?: string; }; + thumbnail?: Partial & { proxy_url?: string; }; + image?: Partial & { proxy_url?: string; }; + video?: Partial & { proxy_url?: string; }; + footer?: Partial & { icon_url?: string; proxy_icon_url?: string; }; } interface MessageEmbedAuthor { @@ -2629,8 +2629,8 @@ declare module 'discord.js' { partial: true; fetch(): Promise; } & { - [K in keyof Omit]: T[K] | null; - }; + [K in keyof Omit]: T[K] | null; + }; interface PartialMessage extends Partialize {} interface PartialChannel extends Partialize {} From 98a552107eb980de427621c3444c29e5a0691a25 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 17:56:44 +0100 Subject: [PATCH 1318/1359] tooling(typings): new linter for typings (#3827) --- package.json | 2 +- tsconfig.json | 26 ++++-- tslint.json | 80 ++++++------------ typings/index.d.ts | 204 ++++++++++++++++++++------------------------- 4 files changed, 134 insertions(+), 178 deletions(-) diff --git a/package.json b/package.json index b41d42c35..f693c27f1 100644 --- a/package.json +++ b/package.json @@ -76,12 +76,12 @@ "@types/node": "^10.12.24", "@types/ws": "^6.0.1", "discord.js-docgen": "discordjs/docgen", + "dtslint": "^3.0.0", "eslint": "^5.13.0", "jest": "^24.7.1", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", "tslint": "^5.12.1", - "tslint-config-typings": "^0.3.1", "typescript": "^3.3.3", "webpack": "^4.29.3", "webpack-cli": "^3.2.3" diff --git a/tsconfig.json b/tsconfig.json index 37aa901ee..dbba5126e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,21 @@ { "compilerOptions": { + "strict": true, + "moduleResolution": "node", + "declaration": false, + "removeComments": false, + "alwaysStrict": true, + "pretty": true, "module": "commonjs", - "target": "es6", - "noImplicitAny": true, - "strictNullChecks": true, - "noEmit": true, - "forceConsistentCasingInFileNames": true - }, - "files": [ - "typings/index.d.ts" - ] + "target": "es2019", + "lib": [ + "esnext", + "esnext.array", + "esnext.asynciterable", + "esnext.intl", + "esnext.symbol" + ], + "sourceMap": false, + "skipLibCheck": true + } } diff --git a/tslint.json b/tslint.json index 71f850264..98a11a302 100644 --- a/tslint.json +++ b/tslint.json @@ -1,62 +1,30 @@ { "extends": [ - "tslint-config-typings" + "dtslint/dtslint.json" ], "rules": { - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "indent": [ - true, - "tabs" - ], - "no-duplicate-variable": true, - "no-unused-variable": [false], - "no-eval": true, - "no-internal-module": true, - "no-trailing-whitespace": true, - "no-unsafe-finally": true, - "no-var-keyword": true, - "one-line": [ - true, - "check-open-brace", - "check-whitespace" - ], - "quotemark": [ - true, - "single" - ], - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "variable-name": [ - true, - "ban-keywords" - ], - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ] + "prefer-readonly": false, + "await-promise": false, + "no-for-in-array": false, + "no-null-undefined-union": false, + "no-promise-as-boolean": false, + "no-void-expression": false, + "strict-string-expressions": false, + "strict-comparisons": false, + "use-default-type-parameter": false, + "no-boolean-literal-compare": false, + "no-unnecessary-qualifier": false, + "no-unnecessary-type-assertion": false, + "expect": false, + "no-import-default-of-export-equals": false, + "no-relative-import-in-test": false, + "no-unnecessary-generics": false, + "strict-export-declare-modifiers": false, + "no-single-declare-module": false, + "member-access": true, + "no-unnecessary-class": false, + "array-type": [true, "array"], + + "no-any-union": false } } diff --git a/typings/index.d.ts b/typings/index.d.ts index a924fb4fd..d16180e6b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -100,9 +100,9 @@ declare module 'discord.js' { public clearTimeout(timeout: NodeJS.Timer): void; public clearImmediate(timeout: NodeJS.Immediate): void; public destroy(): void; - public setInterval(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; - public setTimeout(fn: Function, delay: number, ...args: any[]): NodeJS.Timer; - public setImmediate(fn: Function, ...args: any[]): NodeJS.Immediate; + public setInterval(fn: (...args: any[]) => void, delay: number, ...args: any[]): NodeJS.Timer; + public setTimeout(fn: (...args: any[]) => void, delay: number, ...args: any[]): NodeJS.Timer; + public setImmediate(fn: (...args: any[]) => void, ...args: any[]): NodeJS.Immediate; public toJSON(...props: { [key: string]: boolean | string; }[]): object; } @@ -182,14 +182,13 @@ declare module 'discord.js' { public on(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public on(event: 'error', listener: (error: Error) => void): this; public on(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; - public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; + public on(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable' | 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public on(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; public on(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public on(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; public on(event: 'inviteCreate' | 'inviteDelete', listener: (invite: Invite) => void): this; - public on(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public on(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; public on(event: 'messageReactionRemoveEmoji', listener: (reaction: MessageReaction) => void): this; public on(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; @@ -197,20 +196,18 @@ declare module 'discord.js' { public on(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public on(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public on(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public on(event: 'ready', listener: () => void): this; + public on(event: 'ready' | 'invalidated', listener: () => void): this; public on(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public on(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public on(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public on(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public on(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public on(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; - public on(event: 'invalidated', listener: () => void): this; public on(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public on(event: 'shardError', listener: (error: Error, id: number) => void): this; - public on(event: 'shardReconnecting', listener: (id: number) => void): this; - public on(event: 'shardReady', listener: (id: number) => void): this; + public on(event: 'shardReady' | 'shardReconnecting', listener: (id: number) => void): this; public on(event: 'shardResume', listener: (id: number, replayed: number) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; public once(event: 'channelCreate' | 'channelDelete', listener: (channel: Channel | PartialChannel) => void): this; public once(event: 'channelPinsUpdate', listener: (channel: Channel | PartialChannel, time: Date) => void): this; @@ -221,33 +218,30 @@ declare module 'discord.js' { public once(event: 'emojiUpdate', listener: (oldEmoji: GuildEmoji, newEmoji: GuildEmoji) => void): this; public once(event: 'error', listener: (error: Error) => void): this; public once(event: 'guildBanAdd' | 'guildBanRemove', listener: (guild: Guild, user: User | PartialUser) => void): this; - public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable', listener: (guild: Guild) => void): this; + public once(event: 'guildCreate' | 'guildDelete' | 'guildUnavailable' | 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'guildMemberAdd' | 'guildMemberAvailable' | 'guildMemberRemove', listener: (member: GuildMember | PartialGuildMember) => void): this; public once(event: 'guildMembersChunk', listener: (members: Collection, guild: Guild) => void): this; public once(event: 'guildMemberSpeaking', listener: (member: GuildMember | PartialGuildMember, speaking: Readonly) => void): this; public once(event: 'guildMemberUpdate', listener: (oldMember: GuildMember | PartialGuildMember, newMember: GuildMember | PartialGuildMember) => void): this; public once(event: 'guildUpdate', listener: (oldGuild: Guild, newGuild: Guild) => void): this; - public once(event: 'guildIntegrationsUpdate', listener: (guild: Guild) => void): this; public once(event: 'message' | 'messageDelete' | 'messageReactionRemoveAll', listener: (message: Message | PartialMessage) => void): this; public once(event: 'messageDeleteBulk', listener: (messages: Collection) => void): this; public once(event: 'messageReactionAdd' | 'messageReactionRemove', listener: (messageReaction: MessageReaction, user: User | PartialUser) => void): this; public once(event: 'messageUpdate', listener: (oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage) => void): this; public once(event: 'presenceUpdate', listener: (oldPresence: Presence | undefined, newPresence: Presence) => void): this; public once(event: 'rateLimit', listener: (rateLimitData: RateLimitData) => void): this; - public once(event: 'ready', listener: () => void): this; + public once(event: 'ready' | 'invalidated', listener: () => void): this; public once(event: 'roleCreate' | 'roleDelete', listener: (role: Role) => void): this; public once(event: 'roleUpdate', listener: (oldRole: Role, newRole: Role) => void): this; public once(event: 'typingStart' | 'typingStop', listener: (channel: Channel | PartialChannel, user: User | PartialUser) => void): this; public once(event: 'userUpdate', listener: (oldUser: User | PartialUser, newUser: User | PartialUser) => void): this; public once(event: 'voiceStateUpdate', listener: (oldState: VoiceState, newState: VoiceState) => void): this; public once(event: 'webhookUpdate', listener: (channel: TextChannel) => void): this; - public once(event: 'invalidated', listener: () => void): this; public once(event: 'shardDisconnect', listener: (event: CloseEvent, id: number) => void): this; public once(event: 'shardError', listener: (error: Error, id: number) => void): this; - public once(event: 'shardReconnecting', listener: (id: number) => void): this; - public once(event: 'shardReady', listener: (id: number) => void): this; + public once(event: 'shardReady' | 'shardReconnecting', listener: (id: number) => void): this; public once(event: 'shardResume', listener: (id: number, replayed: number) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class ClientVoiceManager { @@ -346,17 +340,15 @@ declare module 'discord.js' { public [Symbol.asyncIterator](): AsyncIterableIterator; public toJSON(): object; - protected listener: Function; + protected listener: (...args: any[]) => void; public abstract collect(...args: any[]): K; public abstract dispose(...args: any[]): K; public abstract endReason(): void; - public on(event: 'collect', listener: (...args: any[]) => void): this; - public on(event: 'dispose', listener: (...args: any[]) => void): this; + public on(event: 'collect' | 'dispose', listener: (...args: any[]) => void): this; public on(event: 'end', listener: (collected: Collection, reason: string) => void): this; - public once(event: 'collect', listener: (...args: any[]) => void): this; - public once(event: 'dispose', listener: (...args: any[]) => void): this; + public once(event: 'collect' | 'dispose', listener: (...args: any[]) => void): this; public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; } @@ -1006,11 +998,9 @@ declare module 'discord.js' { public fetch(): Promise; public pin(): Promise; public react(emoji: EmojiIdentifierResolvable): Promise; - public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - public reply(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + public reply(content?: StringResolvable, options?: MessageOptions | MessageAdditions | MessageOptions & { split?: false; } | MessageAdditions): Promise; public reply(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; - public reply(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - public reply(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + public reply(options?: MessageOptions | MessageAdditions | APIMessage | MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; public reply(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; public suppressEmbeds(suppress?: boolean): Promise; public toJSON(): object; @@ -1185,17 +1175,13 @@ declare module 'discord.js' { public empty(): void; public endReason(): string | null; - public on(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; - public on(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; + public on(event: 'collect' | 'dispose' | 'remove', listener: (reaction: MessageReaction, user: User) => void): this; public on(event: 'end', listener: (collected: Collection, reason: string) => void): this; - public on(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'collect', listener: (reaction: MessageReaction, user: User) => void): this; - public once(event: 'dispose', listener: (reaction: MessageReaction, user: User) => void): this; + public once(event: 'collect' | 'dispose' | 'remove', listener: (reaction: MessageReaction, user: User) => void): this; public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; - public once(event: 'remove', listener: (reaction: MessageReaction, user: User) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class ReactionEmoji extends Emoji { @@ -1252,7 +1238,7 @@ declare module 'discord.js' { export class Shard extends EventEmitter { constructor(manager: ShardingManager, id: number); private _evals: Map>; - private _exitListener: Function; + private _exitListener: (...args: any[]) => void; private _fetches: Map>; private _handleExit(respawn?: boolean): void; private _handleMessage(message: any): void; @@ -1273,19 +1259,17 @@ declare module 'discord.js' { public send(message: any): Promise; public spawn(spawnTimeout?: number): Promise; - public on(event: 'death', listener: (child: ChildProcess) => void): this; + public on(event: 'spawn' | 'death', listener: (child: ChildProcess) => void): this; public on(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; public on(event: 'error', listener: (error: Error) => void): this; public on(event: 'message', listener: (message: any) => void): this; - public on(event: 'spawn', listener: (child: ChildProcess) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'death', listener: (child: ChildProcess) => void): this; + public once(event: 'spawn' | 'death', listener: (child: ChildProcess) => void): this; public once(event: 'disconnect' | 'ready' | 'reconnecting', listener: () => void): this; public once(event: 'error', listener: (error: Error) => void): this; public once(event: 'message', listener: (message: any) => void): this; - public once(event: 'spawn', listener: (child: ChildProcess) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class ShardClientUtil { @@ -1341,7 +1325,7 @@ declare module 'discord.js' { public static generate(timestamp?: number | Date): Snowflake; } - const VolumeMixin: (base: Constructable) => Constructable; + function VolumeMixin(base: Constructable): Constructable; class StreamDispatcher extends VolumeMixin(Writable) { constructor(player: object, options?: StreamOptions, streams?: object); @@ -1360,31 +1344,21 @@ declare module 'discord.js' { public pause(silence?: boolean): void; public resume(): void; - public on(event: 'close', listener: () => void): this; + public on(event: 'close' | 'drain' | 'end' | 'finish' | 'start', listener: () => void): this; public on(event: 'debug', listener: (info: string) => void): this; - public on(event: 'drain', listener: () => void): this; - public on(event: 'end', listener: () => void): this; public on(event: 'error', listener: (err: Error) => void): this; - public on(event: 'finish', listener: () => void): this; - public on(event: 'pipe', listener: (src: Readable) => void): this; - public on(event: 'start', listener: () => void): this; + public on(event: 'pipe' | 'unpipe', listener: (src: Readable) => void): this; public on(event: 'speaking', listener: (speaking: boolean) => void): this; - public on(event: 'unpipe', listener: (src: Readable) => void): this; public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'close', listener: () => void): this; + public once(event: 'close' | 'drain' | 'end' | 'finish' | 'start', listener: () => void): this; public once(event: 'debug', listener: (info: string) => void): this; - public once(event: 'drain', listener: () => void): this; - public once(event: 'end', listener: () => void): this; public once(event: 'error', listener: (err: Error) => void): this; - public once(event: 'finish', listener: () => void): this; - public once(event: 'pipe', listener: (src: Readable) => void): this; - public once(event: 'start', listener: () => void): this; + public once(event: 'pipe' | 'unpipe', listener: (src: Readable) => void): this; public once(event: 'speaking', listener: (speaking: boolean) => void): this; - public once(event: 'unpipe', listener: (src: Readable) => void): this; - public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; - public once(event: string, listener: Function): this; + public once(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class Speaking extends BitField { @@ -1393,10 +1367,10 @@ declare module 'discord.js' { } export class Structures { - static get(structure: K): Extendable[K]; - static get(structure: string): Function; - static extend(structure: K, extender: (baseClass: Extendable[K]) => T): T; - static extend(structure: string, extender: (baseClass: typeof Function) => T): T; + public static get(structure: K): Extendable[K]; + public static get(structure: string): (...args: any[]) => void; + public static extend(structure: K, extender: (baseClass: Extendable[K]) => T): T; + public static extend void>(structure: string, extender: (baseClass: typeof Function) => T): T; } export class SystemChannelFlags extends BitField { @@ -1462,7 +1436,7 @@ declare module 'discord.js' { public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; public static discordSort(collection: Collection): Collection; - public static escapeMarkdown(text: string, options?: { codeBlock?: boolean; inlineCode?: boolean; bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; spoiler?: boolean; inlineCodeContent?: boolean; codeBlockContent?: boolean; }): string; + public static escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string; public static escapeCodeBlock(text: string): string; public static escapeInlineCode(text: string): string; public static escapeBold(text: string): string; @@ -1502,17 +1476,15 @@ declare module 'discord.js' { public on(event: 'end', listener: () => void): this; public on(event: 'error', listener: (error: Error) => void): this; - public on(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; - public on(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public on(event: 'subscribe' | 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; public on(event: 'warn', listener: (warning: string | Error) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; public once(event: 'end', listener: () => void): this; public once(event: 'error', listener: (error: Error) => void): this; - public once(event: 'subscribe', listener: (dispatcher: StreamDispatcher) => void): this; - public once(event: 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; + public once(event: 'subscribe' | 'unsubscribe', listener: (dispatcher: StreamDispatcher) => void): this; public once(event: 'warn', listener: (warning: string | Error) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class VoiceChannel extends GuildChannel { @@ -1563,31 +1535,19 @@ declare module 'discord.js' { public disconnect(): void; public play(input: VoiceBroadcast | Readable | string, options?: StreamOptions): StreamDispatcher; - public on(event: 'authenticated', listener: () => void): this; - public on(event: 'closing', listener: () => void): this; + public on(event: 'authenticated' | 'closing' | 'newSession' | 'ready' | 'reconnecting', listener: () => void): this; public on(event: 'debug', listener: (message: string) => void): this; - public on(event: 'disconnect', listener: (error: Error) => void): this; - public on(event: 'error', listener: (error: Error) => void): this; - public on(event: 'failed', listener: (error: Error) => void): this; - public on(event: 'newSession', listener: () => void): this; - public on(event: 'ready', listener: () => void): this; - public on(event: 'reconnecting', listener: () => void): this; + public on(event: 'error' | 'failed' | 'disconnect', listener: (error: Error) => void): this; public on(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public on(event: 'warn', listener: (warning: string | Error) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'authenticated', listener: () => void): this; - public once(event: 'closing', listener: () => void): this; + public once(event: 'authenticated' | 'closing' | 'newSession' | 'ready' | 'reconnecting', listener: () => void): this; public once(event: 'debug', listener: (message: string) => void): this; - public once(event: 'disconnect', listener: (error: Error) => void): this; - public once(event: 'error', listener: (error: Error) => void): this; - public once(event: 'failed', listener: (error: Error) => void): this; - public once(event: 'newSession', listener: () => void): this; - public once(event: 'ready', listener: () => void): this; - public once(event: 'reconnecting', listener: () => void): this; + public once(event: 'error' | 'failed' | 'disconnect', listener: (error: Error) => void): this; public once(event: 'speaking', listener: (user: User, speaking: Readonly) => void): this; public once(event: 'warn', listener: (warning: string | Error) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } class VoiceReceiver extends EventEmitter { @@ -1595,10 +1555,10 @@ declare module 'discord.js' { public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual'; }): Readable; public on(event: 'debug', listener: (error: Error | string) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; public once(event: 'debug', listener: (error: Error | string) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } export class VoiceRegion { @@ -1741,19 +1701,15 @@ declare module 'discord.js' { private _emitDestroyed(): void; public send(data: object): void; - public on(event: 'ready', listener: () => void): this; - public on(event: 'resumed', listener: () => void): this; + public on(event: 'ready' | 'resumed' | 'invalidSession', listener: () => void): this; public on(event: 'close', listener: (event: CloseEvent) => void): this; - public on(event: 'invalidSession', listener: () => void): this; public on(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; - public on(event: string, listener: Function): this; + public on(event: string, listener: (...args: any[]) => void): this; - public once(event: 'ready', listener: () => void): this; - public once(event: 'resumed', listener: () => void): this; + public once(event: 'ready' | 'resumed' | 'invalidSession', listener: () => void): this; public once(event: 'close', listener: (event: CloseEvent) => void): this; - public once(event: 'invalidSession', listener: () => void): this; public once(event: 'allReady', listener: (unavailableGuilds?: Set) => void): this; - public once(event: string, listener: Function): this; + public once(event: string, listener: (...args: any[]) => void): this; } //#endregion @@ -1761,12 +1717,10 @@ declare module 'discord.js' { //#region Collections export class Collection extends BaseCollection { - public flatMap(fn: (value: V, key: K, collection: this) => Collection): Collection; - public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; public flatMap(fn: (value: V, key: K, collection: this) => Collection, thisArg?: unknown): Collection; - public mapValues(fn: (value: V, key: K, collection: this) => T): Collection; - public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; + public flatMap(fn: (this: This, value: V, key: K, collection: this) => Collection, thisArg: This): Collection; public mapValues(fn: (value: V, key: K, collection: this) => T, thisArg?: unknown): Collection; + public mapValues(fn: (this: This, value: V, key: K, collection: this) => T, thisArg: This): Collection; public toJSON(): object; } @@ -1914,8 +1868,8 @@ declare module 'discord.js' { // to each of those classes type Constructable = new (...args: any[]) => T; - const PartialTextBasedChannel: (Base?: Constructable) => Constructable; - const TextBasedChannel: (Base?: Constructable) => Constructable; + function PartialTextBasedChannel(Base?: Constructable): Constructable; + function TextBasedChannel(Base?: Constructable): Constructable; interface PartialTextBasedChannelFields { lastMessageID: Snowflake | null; @@ -1923,11 +1877,9 @@ declare module 'discord.js' { readonly lastMessage: Message | null; lastPinTimestamp: number | null; readonly lastPinAt: Date; - send(content?: StringResolvable, options?: MessageOptions | MessageAdditions): Promise; - send(content?: StringResolvable, options?: MessageOptions & { split?: false; } | MessageAdditions): Promise; + send(content?: StringResolvable, options?: MessageOptions | MessageAdditions | MessageOptions & { split?: false; } | MessageAdditions): Promise; send(content?: StringResolvable, options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions): Promise; - send(options?: MessageOptions | MessageAdditions | APIMessage): Promise; - send(options?: MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; + send(options?: MessageOptions | MessageAdditions | APIMessage | MessageOptions & { split?: false; } | MessageAdditions | APIMessage): Promise; send(options?: MessageOptions & { split: true | SplitOptions; } | MessageAdditions | APIMessage): Promise; } @@ -1941,7 +1893,7 @@ declare module 'discord.js' { stopTyping(force?: boolean): void; } - const WebhookMixin: (Base?: Constructable) => Constructable; + function WebhookMixin(Base?: Constructable): Constructable; interface WebhookFields { readonly client: Client; @@ -2787,9 +2739,25 @@ declare module 'discord.js' { | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; - type MessageEvent = { data: WebSocket.Data; type: string; target: WebSocket; }; - type CloseEvent = { wasClean: boolean; code: number; reason: string; target: WebSocket; }; - type ErrorEvent = { error: any, message: string, type: string, target: WebSocket; }; + interface MessageEvent { + data: WebSocket.Data; + type: string; + target: WebSocket; + } + + interface CloseEvent { + wasClean: boolean; + code: number; + reason: string; + target: WebSocket; + } + + interface ErrorEvent { + error: any; + message: string; + type: string; + target: WebSocket; + } interface CrosspostedChannel { channelID: Snowflake; @@ -2798,5 +2766,17 @@ declare module 'discord.js' { name: string; } + interface EscapeMarkdownOptions { + codeBlock?: boolean; + inlineCode?: boolean; + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + spoiler?: boolean; + inlineCodeContent?: boolean; + codeBlockContent?: boolean; + } + //#endregion } From 0a1b9a52853213fe9e6dc14a4a8f11b1ec5f9056 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:15:38 +0000 Subject: [PATCH 1319/1359] refactor: remove unused error in catch statements (#3820) * refactor(handlers): remove unused error in catch * refactor(PacketHandler): remove unused error * refactor(SecretBox): remove unused error * refactor(ClientPresence): remove unused error * style: remove space Co-Authored-By: Crawl Co-authored-by: Crawl --- src/client/voice/receiver/PacketHandler.js | 2 +- src/client/voice/util/Secretbox.js | 2 +- src/client/websocket/handlers/index.js | 2 +- src/structures/ClientPresence.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/voice/receiver/PacketHandler.js b/src/client/voice/receiver/PacketHandler.js index d468ababc..3825b2ac3 100644 --- a/src/client/voice/receiver/PacketHandler.js +++ b/src/client/voice/receiver/PacketHandler.js @@ -90,7 +90,7 @@ class PacketHandler extends EventEmitter { this.connection.onSpeaking({ user_id: userStat.userID, ssrc: ssrc, speaking: 0 }); this.receiver.connection.client.clearTimeout(speakingTimeout); this.speakingTimeouts.delete(ssrc); - } catch (ex) { + } catch { // Connection already closed, ignore } }, DISCORD_SPEAKING_DELAY); diff --git a/src/client/voice/util/Secretbox.js b/src/client/voice/util/Secretbox.js index 1b30eeb60..c16a43531 100644 --- a/src/client/voice/util/Secretbox.js +++ b/src/client/voice/util/Secretbox.js @@ -27,6 +27,6 @@ exports.methods = {}; if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; // eslint-disable-line no-await-in-loop exports.methods = libs[libName](lib); break; - } catch (err) {} // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty } })(); diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index b253cefee..d69c105fd 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -7,7 +7,7 @@ const handlers = {}; for (const name of Object.keys(WSEvents)) { try { handlers[name] = require(`./${name}.js`); - } catch (err) {} // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty } module.exports = handlers; diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 213b46f2d..3fdfd5b22 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -39,7 +39,7 @@ class ClientPresence extends Presence { try { const a = await this.client.api.oauth2.applications(applicationID).assets.get(); for (const asset of a) assets.set(asset.name, asset.id); - } catch (err) { } // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty } } From a69ebbe9d957a21d165caf0582c0640e501908e9 Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 24 Feb 2020 18:16:20 +0100 Subject: [PATCH 1320/1359] feat/fix(GuildAuditLogs): handle new event types (#3602) * feat/fix(GuildAuditLogs): handle new event types * fix(GuildAuditLogsEntry): coerce to numbers, simplify extra for deleted entities * fix(GuildAuditLogsEntry): do not revert 'type' extra --- src/structures/GuildAuditLogs.js | 109 ++++++++++++++++++++++++------- typings/index.d.ts | 5 +- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index a6327f35c..7a83d4958 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -1,10 +1,11 @@ 'use strict'; const Collection = require('../util/Collection'); +const Integration = require('./Integration'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); const Util = require('../util/Util'); -const PartialTypes = require('../util/Constants'); +const { PartialTypes } = require('../util/Constants'); /** * The target type of an entry, e.g. `GUILD`. Here are the available types: @@ -16,6 +17,7 @@ const PartialTypes = require('../util/Constants'); * * WEBHOOK * * EMOJI * * MESSAGE + * * INTEGRATION * @typedef {string} AuditLogTargetType */ @@ -34,6 +36,7 @@ const Targets = { WEBHOOK: 'WEBHOOK', EMOJI: 'EMOJI', MESSAGE: 'MESSAGE', + INTEGRATION: 'INTEGRATION', UNKNOWN: 'UNKNOWN', }; @@ -141,6 +144,18 @@ class GuildAuditLogs { } } + /** + * Cached integrations + * @type {Collection} + * @private + */ + this.integrations = new Collection(); + if (data.integrations) { + for (const integration of data.integrations) { + this.integrations.set(integration.id, new Integration(guild.client, integration, guild)); + } + } + /** * The entries for this guild's audit logs * @type {Collection} @@ -169,9 +184,10 @@ class GuildAuditLogs { * * An emoji * * An invite * * A webhook + * * An integration * * An object with an id key if target was deleted * * An object where the keys represent either the new value or the old value - * @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook} AuditLogEntryTarget + * @typedef {?Object|Guild|User|Role|GuildEmoji|Invite|Webhook|Integration} AuditLogEntryTarget */ /** @@ -188,6 +204,7 @@ class GuildAuditLogs { if (target < 60) return Targets.WEBHOOK; if (target < 70) return Targets.EMOJI; if (target < 80) return Targets.MESSAGE; + if (target < 90) return Targets.INTEGRATION; return Targets.UNKNOWN; } @@ -210,10 +227,13 @@ class GuildAuditLogs { Actions.CHANNEL_CREATE, Actions.CHANNEL_OVERWRITE_CREATE, Actions.MEMBER_BAN_REMOVE, + Actions.BOT_ADD, Actions.ROLE_CREATE, Actions.INVITE_CREATE, Actions.WEBHOOK_CREATE, Actions.EMOJI_CREATE, + Actions.MESSAGE_PIN, + Actions.INTEGRATION_CREATE, ].includes(action)) return 'CREATE'; if ([ @@ -222,11 +242,15 @@ class GuildAuditLogs { Actions.MEMBER_KICK, Actions.MEMBER_PRUNE, Actions.MEMBER_BAN_ADD, + Actions.MEMBER_DISCONNECT, Actions.ROLE_DELETE, Actions.INVITE_DELETE, Actions.WEBHOOK_DELETE, Actions.EMOJI_DELETE, Actions.MESSAGE_DELETE, + Actions.MESSAGE_BULK_DELETE, + Actions.MESSAGE_UNPIN, + Actions.INTEGRATION_DELETE, ].includes(action)) return 'DELETE'; if ([ @@ -235,10 +259,12 @@ class GuildAuditLogs { Actions.CHANNEL_OVERWRITE_UPDATE, Actions.MEMBER_UPDATE, Actions.MEMBER_ROLE_UPDATE, + Actions.MEMBER_MOVE, Actions.ROLE_UPDATE, Actions.INVITE_UPDATE, Actions.WEBHOOK_UPDATE, Actions.EMOJI_UPDATE, + Actions.INTEGRATION_UPDATE, ].includes(action)) return 'UPDATE'; return 'ALL'; @@ -312,49 +338,73 @@ class GuildAuditLogsEntry { * @type {?Object|Role|GuildMember} */ this.extra = null; - if (data.options) { - if (data.action_type === Actions.MEMBER_PRUNE) { + switch (data.action_type) { + case Actions.MEMBER_PRUNE: this.extra = { - removed: data.options.members_removed, - days: data.options.delete_member_days, + removed: Number(data.options.members_removed), + days: Number(data.options.delete_member_days), }; - } else if (data.action_type === Actions.MESSAGE_DELETE) { + break; + + case Actions.MEMBER_MOVE: + case Actions.MESSAGE_DELETE: + case Actions.MESSAGE_BULK_DELETE: this.extra = { - count: data.options.count, - channel: guild.channels.cache.get(data.options.channel_id), + channel: guild.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, + count: Number(data.options.count), }; - } else if (data.action_type === Actions.MESSAGE_BULK_DELETE) { + break; + + case Actions.MESSAGE_PIN: + case Actions.MESSAGE_UNPIN: this.extra = { - count: data.options.count, + channel: guild.client.channels.cache.get(data.options.channel_id) || { id: data.options.channel_id }, + messageID: data.options.message_id, }; - } else { + break; + + case Actions.MEMBER_DISCONNECT: + this.extra = { + count: Number(data.options.count), + }; + break; + + case Actions.CHANNEL_OVERWRITE_CREATE: + case Actions.CHANNEL_OVERWRITE_UPDATE: + case Actions.CHANNEL_OVERWRITE_DELETE: switch (data.options.type) { case 'member': - this.extra = guild.members.cache.get(data.options.id); - if (!this.extra) this.extra = { id: data.options.id }; + this.extra = guild.members.cache.get(data.options.id) || + { id: data.options.id, type: 'member' }; break; + case 'role': - this.extra = guild.roles.cache.get(data.options.id); - if (!this.extra) this.extra = { id: data.options.id, name: data.options.role_name }; + this.extra = guild.roles.cache.get(data.options.id) || + { id: data.options.id, name: data.options.role_name, type: 'role' }; break; + default: break; } - } + break; + + default: + break; } - + /** + * The target of this entry + * @type {?AuditLogEntryTarget} + */ + this.target = null; if (targetType === Targets.UNKNOWN) { - /** - * The target of this entry - * @type {AuditLogEntryTarget} - */ this.target = this.changes.reduce((o, c) => { o[c.key] = c.new || c.old; return o; }, {}); this.target.id = data.target_id; - } else if (targetType === Targets.USER) { + // MEMBER_DISCONNECT and similar types do not provide a target_id. + } else if (targetType === Targets.USER && data.target_id) { this.target = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users.add({ id: data.target_id }) : guild.client.users.cache.get(data.target_id); @@ -386,8 +436,17 @@ class GuildAuditLogsEntry { } }); } else if (targetType === Targets.MESSAGE) { - this.target = guild.client.users.cache.get(data.target_id); - } else { + // Discord sends a channel id for the MESSAGE_BULK_DELETE action type. + this.target = data.action_type === Actions.MESSAGE_BULK_DELETE ? + guild.channels.cache.get(data.target_id) || { id: data.target_id } : + guild.client.users.cache.get(data.target_id); + } else if (targetType === Targets.INTEGRATION) { + this.target = logs.integrations.get(data.target_id) || + new Integration(guild.client, this.changes.reduce((o, c) => { + o[c.key] = c.new || c.old; + return o; + }, { id: data.target_id }), guild); + } else if (data.target_id) { this.target = guild[`${targetType.toLowerCase()}s`].cache.get(data.target_id) || { id: data.target_id }; } } diff --git a/typings/index.d.ts b/typings/index.d.ts index d16180e6b..0788fd057 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -765,6 +765,7 @@ declare module 'discord.js' { export class GuildAuditLogs { constructor(guild: Guild, data: object); private webhooks: Collection; + private integrations: Collection; public entries: Collection; @@ -788,7 +789,7 @@ declare module 'discord.js' { public extra: object | Role | GuildMember | null; public id: Snowflake; public reason: string | null; - public target: Guild | User | Role | GuildEmoji | Invite | Webhook; + public target: Guild | User | Role | GuildEmoji | Invite | Webhook | Integration | null; public targetType: GuildAuditLogsTarget; public toJSON(): object; } @@ -2258,6 +2259,8 @@ declare module 'discord.js' { WEBHOOK?: string; EMOJI?: string; MESSAGE?: string; + INTEGRATION?: string; + UNKNOWN?: string; } type GuildChannelResolvable = Snowflake | GuildChannel; From acf724e691678998cb6774846454913a183c9614 Mon Sep 17 00:00:00 2001 From: matthewfripp <50251454+matthewfripp@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:17:24 +0000 Subject: [PATCH 1321/1359] feat(Collector): Addition of resetTimer() (#3825) * feat(Collector): Addition of resetTimer() * typings --- src/structures/interfaces/Collector.js | 17 +++++++++++++++++ typings/index.d.ts | 1 + 2 files changed, 18 insertions(+) diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 3cd5d387e..6231605e7 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -188,6 +188,23 @@ class Collector extends EventEmitter { this.emit('end', this.collected, reason); } + /** + * Resets the collectors timeout and idle timer. + * @param {Object} [options] Options + * @param {number} [options.time] How long to run the collector for in milliseconds + * @param {number} [options.idle] How long to stop the collector after inactivity in milliseconds + */ + resetTimer({ time, idle } = {}) { + if (this._timeout) { + this.client.clearTimeout(this._timeout); + this._timeout = this.client.setTimeout(() => this.stop('time'), time || this.options.time); + } + if (this._idletimeout) { + this.client.clearTimeout(this._idletimeout); + this._idletimeout = this.client.setTimeout(() => this.stop('idle'), idle || this.options.idle); + } + } + /** * Checks whether the collector should end, and if so, ends it. */ diff --git a/typings/index.d.ts b/typings/index.d.ts index 0788fd057..1b12e82d2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -337,6 +337,7 @@ declare module 'discord.js' { public handleCollect(...args: any[]): void; public handleDispose(...args: any[]): void; public stop(reason?: string): void; + public resetTimer(options?: { time?: number, idle?: number }): void; public [Symbol.asyncIterator](): AsyncIterableIterator; public toJSON(): object; From 52c0a4067b4950aaa655488b112ddfd51c06264b Mon Sep 17 00:00:00 2001 From: BorgerKing <38166539+RDambrosio016@users.noreply.github.com> Date: Mon, 24 Feb 2020 12:21:29 -0500 Subject: [PATCH 1322/1359] fix(MessageEmbed): various typos and fixes (#3819) * fix: typo * fix: couple more typos * fix: grammar stuff * fix: EmbedField takes StringResolvable not string * Revert "fix: EmbedField takes StringResolvable not string" This reverts commit c1bdd78ad378a4c2e2f9772753c24aaab0c2d910. Co-authored-by: Crawl --- src/structures/MessageEmbed.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 7cfb6d1aa..b5ce08b26 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -215,7 +215,7 @@ class MessageEmbed { } /** - * Adds a fields to the embed (max 25). + * Adds fields to the embed (max 25). * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add * @returns {MessageEmbed} */ @@ -372,7 +372,7 @@ class MessageEmbed { } /** - * Checks for valid field input and resolves strings + * Normalizes field input and resolves strings. * @param {StringResolvable} name The name of the field * @param {StringResolvable} value The value of the field * @param {boolean} [inline=false] Set the field to display inline @@ -394,7 +394,7 @@ class MessageEmbed { */ /** - * Check for valid field input and resolves strings + * Normalizes field input and resolves strings. * @param {...EmbedFieldData|EmbedFieldData[]} fields Fields to normalize * @returns {EmbedField[]} */ From 02807347e7e899f476780d8632ddd62bffae11fa Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:27:15 +0000 Subject: [PATCH 1323/1359] fix: Client#sweepMessages should throw an INVALID_TYPE error (#3828) * fix(Client): sweepMessages shouldn't shrow an invalid client option error * style: trailing commas --- src/client/Client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index ae7bbf8b3..ec5128860 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -64,7 +64,7 @@ class Client extends BaseClient { if (Array.isArray(this.options.shards)) { this.options.shards = [...new Set( - this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)) + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)), )]; } @@ -199,7 +199,7 @@ class Client extends BaseClient { if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID'); this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); this.emit(Events.DEBUG, - `Provided token: ${token.split('.').map((val, i) => i > 1 ? val.replace(/./g, '*') : val).join('.')}` + `Provided token: ${token.split('.').map((val, i) => i > 1 ? val.replace(/./g, '*') : val).join('.')}`, ); if (this.options.presence) { @@ -286,7 +286,7 @@ class Client extends BaseClient { */ sweepMessages(lifetime = this.options.messageCacheLifetime) { if (typeof lifetime !== 'number' || isNaN(lifetime)) { - throw new TypeError('CLIENT_INVALID_OPTION', 'Lifetime', 'a number'); + throw new TypeError('INVALID_TYPE', 'lifetime', 'number'); } if (lifetime <= 0) { this.emit(Events.DEBUG, 'Didn\'t sweep messages - lifetime is unlimited'); @@ -303,7 +303,7 @@ class Client extends BaseClient { channels++; messages += channel.messages.cache.sweep( - message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs + message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs, ); } From 91a025caaaadb49a4f6610ef1ff2f06d69784987 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 17:27:34 +0000 Subject: [PATCH 1324/1359] feat: GuildEmoji & Invite to GuildResolvable (#3637) * Add GuildEmoji to GuildResolvable * Add GuildEmoji to GuildResolvable * Add Invite to GuildResolvable * Add Invite to GuildResolvable * oops * oops x2 * Add Guild#fetchBan and an error for not resolving the ID * typings * Revert "Add Guild#fetchBan and an error for not resolving the ID" This reverts commit a4d0ed16e788beb18074cfc6f0cc72f27c325d56. * Revert "typings" This reverts commit 5a54e88785f5284b49ab96263134bfebb05df4e0. * fix jsdoc * add trailing comma --- src/managers/GuildManager.js | 16 ++++++++++++---- typings/index.d.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index c2747580e..8a28e06b2 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -6,6 +6,8 @@ const { Events } = require('../util/Constants'); const Guild = require('../structures/Guild'); const GuildChannel = require('../structures/GuildChannel'); const GuildMember = require('../structures/GuildMember'); +const GuildEmoji = require('../structures/GuildEmoji'); +const Invite = require('../structures/Invite'); const Role = require('../structures/Role'); /** @@ -27,9 +29,11 @@ class GuildManager extends BaseManager { * Data that resolves to give a Guild object. This can be: * * A Guild object * * A GuildChannel object + * * A GuildEmoji object * * A Role object * * A Snowflake - * @typedef {Guild|GuildChannel|GuildMember|Role|Snowflake} GuildResolvable + * * An Invite object + * @typedef {Guild|GuildChannel|GuildMember|GuildEmoji|Role|Snowflake|Invite} GuildResolvable */ /** @@ -43,7 +47,9 @@ class GuildManager extends BaseManager { resolve(guild) { if (guild instanceof GuildChannel || guild instanceof GuildMember || - guild instanceof Role) return super.resolve(guild.guild); + guild instanceof GuildEmoji || + guild instanceof Role || + (guild instanceof Invite && guild.guild)) return super.resolve(guild.guild); return super.resolve(guild); } @@ -58,7 +64,9 @@ class GuildManager extends BaseManager { resolveID(guild) { if (guild instanceof GuildChannel || guild instanceof GuildMember || - guild instanceof Role) return super.resolveID(guild.guild.id); + guild instanceof GuildEmoji || + guild instanceof Role || + (guild instanceof Invite && guild.guild)) return super.resolveID(guild.guild.id); return super.resolveID(guild); } @@ -92,7 +100,7 @@ class GuildManager extends BaseManager { resolve(this.client.guilds.add(data)); }, 10000); return undefined; - }, reject) + }, reject), ); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 1b12e82d2..ecfa6f1d2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2332,7 +2332,7 @@ declare module 'discord.js' { type GuildMemberResolvable = GuildMember | UserResolvable; - type GuildResolvable = Guild | GuildChannel | GuildMember | Role | Snowflake; + type GuildResolvable = Guild | GuildChannel | GuildMember | GuildEmoji | Invite | Role | Snowflake; interface GuildPruneMembersOptions { count?: boolean; From 44ff67dc1192e4ca9aa1cf69390d0453c5302f0c Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Mon, 24 Feb 2020 21:01:41 +0000 Subject: [PATCH 1325/1359] typings(WebhookClient): client is not a Client (#3829) * typings(WebhookClient): client is not a Client * style: use tabs --- typings/index.d.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index ecfa6f1d2..605969f57 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -303,13 +303,6 @@ declare module 'discord.js' { public toString(): string; } - export interface ActivityOptions { - name?: string; - url?: string; - type?: ActivityType | number; - shardID?: number | number[]; - } - export class ClientUser extends User { public mfaEnabled: boolean; public verified: boolean; @@ -1630,6 +1623,7 @@ declare module 'discord.js' { export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); public token: string; + public readonly client: this; } export class WebSocketManager extends EventEmitter { @@ -1923,6 +1917,13 @@ declare module 'discord.js' { | 'SYNC' | 'PLAY'; + interface ActivityOptions { + name?: string; + url?: string; + type?: ActivityType | number; + shardID?: number | number[]; + } + type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' From c1d396a6c4c063a70f28bda6498b1114837af6b0 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 22:44:46 +0100 Subject: [PATCH 1326/1359] Partial-revert "typings(WebhookClient): client is not a Client" (#3831) * Revert "typings(WebhookClient): client is not a Client (#3829)" This reverts commit 44ff67dc1192e4ca9aa1cf69390d0453c5302f0c. * Update index.d.ts * Update index.d.ts --- typings/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 605969f57..95b0790c7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1623,7 +1623,6 @@ declare module 'discord.js' { export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); public token: string; - public readonly client: this; } export class WebSocketManager extends EventEmitter { From 3a0470b45c989a565e2dd3d827282f1a07cee6e1 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 23:14:31 +0100 Subject: [PATCH 1327/1359] chore(deps): update deps and fix lint (#3833) --- package.json | 30 +++++++++---------- src/client/voice/util/VolumeInterface.js | 2 +- src/client/websocket/handlers/GUILD_CREATE.js | 4 +-- src/rest/RequestHandler.js | 6 ++-- src/sharding/Shard.js | 4 +-- src/structures/Guild.js | 10 +++---- src/structures/GuildChannel.js | 1 + src/structures/Message.js | 8 ++--- src/structures/interfaces/TextBasedChannel.js | 4 +-- src/util/Snowflake.js | 2 +- src/util/Structures.js | 4 +-- src/util/Util.js | 2 +- 12 files changed, 39 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index f693c27f1..721dc662c 100644 --- a/package.json +++ b/package.json @@ -35,19 +35,19 @@ "runkitExampleFilename": "./docs/examples/ping.js", "unpkg": "./webpack/discord.min.js", "dependencies": { - "@discordjs/collection": "^0.1.1", + "@discordjs/collection": "^0.1.5", "abort-controller": "^3.0.0", - "form-data": "^2.3.3", - "node-fetch": "^2.3.0", - "prism-media": "^1.0.0", + "form-data": "^3.0.0", + "node-fetch": "^2.6.0", + "prism-media": "^1.2.0", "setimmediate": "^1.0.5", - "tweetnacl": "^1.0.1", - "ws": "^7.2.0" + "tweetnacl": "^1.0.3", + "ws": "^7.2.1" }, "peerDependencies": { "bufferutil": "^4.0.1", "erlpack": "discordapp/erlpack", - "libsodium-wrappers": "^0.7.4", + "libsodium-wrappers": "^0.7.6", "sodium": "^3.0.2", "utf-8-validate": "^5.0.2", "zlib-sync": "^0.1.6" @@ -74,20 +74,20 @@ }, "devDependencies": { "@types/node": "^10.12.24", - "@types/ws": "^6.0.1", + "@types/ws": "^7.2.1", "discord.js-docgen": "discordjs/docgen", "dtslint": "^3.0.0", - "eslint": "^5.13.0", - "jest": "^24.7.1", + "eslint": "^6.8.0", + "jest": "^25.1.0", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", - "tslint": "^5.12.1", - "typescript": "^3.3.3", - "webpack": "^4.29.3", - "webpack-cli": "^3.2.3" + "tslint": "^6.0.0", + "typescript": "^3.8.2", + "webpack": "^4.41.6", + "webpack-cli": "^3.3.11" }, "engines": { - "node": ">=11.0.0" + "node": ">=12.0.0" }, "browser": { "@discordjs/opus": false, diff --git a/src/client/voice/util/VolumeInterface.js b/src/client/voice/util/VolumeInterface.js index ba162a947..179d4fb78 100644 --- a/src/client/voice/util/VolumeInterface.js +++ b/src/client/voice/util/VolumeInterface.js @@ -106,7 +106,7 @@ exports.applyToClass = function applyToClass(structure) { Object.defineProperty( structure.prototype, prop, - Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop) + Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop), ); } }; diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index eb8974435..9994d9f84 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -11,7 +11,7 @@ module.exports = async (client, { d: data }, shard) => { // If the client was ready before and we had unavailable guilds, fetch them if (client.ws.status === Status.READY && client.options.fetchAllMembers) { await guild.members.fetch().catch(err => - client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`), ); } } @@ -27,7 +27,7 @@ module.exports = async (client, { d: data }, shard) => { */ if (client.options.fetchAllMembers) { await guild.members.fetch().catch(err => - client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`) + client.emit(Events.DEBUG, `Failed to fetch all members: ${err}\n${err.stack}`), ); } client.emit(Events.GUILD_CREATE, guild); diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index 8f650b42a..8a8ecf9a7 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -103,7 +103,7 @@ class RequestHandler { // NodeFetch error expected for all "operational" errors, such as 500 status code this.busy = false; return reject( - new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path) + new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path), ); } @@ -155,7 +155,7 @@ class RequestHandler { // Retry the specified number of times for possible serverside issues if (item.retries === this.manager.client.options.retryLimit) { return reject( - new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path) + new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path), ); } else { item.retries++; @@ -172,7 +172,7 @@ class RequestHandler { return null; } catch (err) { return reject( - new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path) + new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path), ); } } diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 04d224db4..7e016fbee 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -294,7 +294,7 @@ class Shard extends EventEmitter { if (message._sFetchProp) { this.manager.fetchClientValues(message._sFetchProp).then( results => this.send({ _sFetchProp: message._sFetchProp, _result: results }), - err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }) + err => this.send({ _sFetchProp: message._sFetchProp, _error: Util.makePlainError(err) }), ); return; } @@ -303,7 +303,7 @@ class Shard extends EventEmitter { if (message._sEval) { this.manager.broadcastEval(message._sEval).then( results => this.send({ _sEval: message._sEval, _result: results }), - err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }) + err => this.send({ _sEval: message._sEval, _error: Util.makePlainError(err) }), ); return; } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index fc04530c9..9353f4de5 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -616,7 +616,7 @@ class Guild extends Base { user: this.client.users.add(ban.user), }); return collection; - }, new Collection()) + }, new Collection()), ); } @@ -634,7 +634,7 @@ class Guild extends Base { return this.client.api.guilds(this.id).integrations.get().then(data => data.reduce((collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)), - new Collection()) + new Collection()), ); } @@ -1086,7 +1086,7 @@ class Guild extends Base { this.client.actions.GuildChannelsPositionUpdate.handle({ guild_id: this.id, channels: updatedChannels, - }).guild + }).guild, ); } @@ -1120,7 +1120,7 @@ class Guild extends Base { this.client.actions.GuildRolePositionUpdate.handle({ guild_id: this.id, roles: rolePositions, - }).guild + }).guild, ); } @@ -1250,7 +1250,7 @@ class Guild extends Base { _sortedChannels(channel) { const category = channel.type === ChannelTypes.CATEGORY; return Util.discordSort(this.channels.cache.filter(c => - c.type === channel.type && (category || c.parent === channel.parent) + c.type === channel.type && (category || c.parent === channel.parent), )); } } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 649e5b222..35a0bc2ab 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -375,6 +375,7 @@ class GuildChannel extends Channel { */ setParent(channel, { lockPermissions = true, reason } = {}) { return this.edit({ + // eslint-disable-next-line no-prototype-builtins parentID: channel !== null ? channel.hasOwnProperty('id') ? channel.id : channel : null, lockPermissions, }, reason); diff --git a/src/structures/Message.js b/src/structures/Message.js index 0c856fdae..d32e1c017 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -108,7 +108,7 @@ class Message extends Base { if (data.attachments) { for (const attachment of data.attachments) { this.attachments.set(attachment.id, new MessageAttachment( - attachment.url, attachment.filename, attachment + attachment.url, attachment.filename, attachment, )); } } @@ -230,7 +230,7 @@ class Message extends Base { this.attachments = new Collection(); for (const attachment of data.attachments) { this.attachments.set(attachment.id, new MessageAttachment( - attachment.url, attachment.filename, attachment + attachment.url, attachment.filename, attachment, )); } } else { @@ -242,7 +242,7 @@ class Message extends Base { 'mentions' in data ? data.mentions : this.mentions.users, 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone, - 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels + 'mention_channels' in data ? data.mention_channels : this.mentions.crosspostedChannels, ); this.flags = new MessageFlags('flags' in data ? data.flags : 0).freeze(); @@ -511,7 +511,7 @@ class Message extends Base { reply(content, options) { return this.channel.send(content instanceof APIMessage ? content : - APIMessage.transformOptions(content, options, { reply: this.member || this.author }) + APIMessage.transformOptions(content, options, { reply: this.member || this.author }), ); } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 4106e00df..0fe0e2dca 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -300,7 +300,7 @@ class TextBasedChannel { let messageIDs = messages instanceof Collection ? messages.keyArray() : messages.map(m => m.id || m); if (filterOld) { messageIDs = messageIDs.filter(id => - Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000 + Date.now() - Snowflake.deconstruct(id).date.getTime() < 1209600000, ); } if (messageIDs.length === 0) return new Collection(); @@ -336,7 +336,7 @@ class TextBasedChannel { 'typing', 'typingCount', 'createMessageCollector', - 'awaitMessages' + 'awaitMessages', ); } for (const prop of props) { diff --git a/src/util/Snowflake.js b/src/util/Snowflake.js index 06612c30b..71090adc0 100644 --- a/src/util/Snowflake.js +++ b/src/util/Snowflake.js @@ -36,7 +36,7 @@ class SnowflakeUtil { if (timestamp instanceof Date) timestamp = timestamp.getTime(); if (typeof timestamp !== 'number' || isNaN(timestamp)) { throw new TypeError( - `"timestamp" argument must be a number (received ${isNaN(timestamp) ? 'NaN' : typeof timestamp})` + `"timestamp" argument must be a number (received ${isNaN(timestamp) ? 'NaN' : typeof timestamp})`, ); } if (INCREMENT >= 4095) INCREMENT = 0; diff --git a/src/util/Structures.js b/src/util/Structures.js index c4061f62c..fdbab0697 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -45,7 +45,7 @@ class Structures { if (typeof extender !== 'function') { const received = `(received ${typeof extender})`; throw new TypeError( - `"extender" argument must be a function that returns the extended structure class/prototype ${received}.` + `"extender" argument must be a function that returns the extended structure class/prototype ${received}.`, ); } @@ -60,7 +60,7 @@ class Structures { const received = `${extended.name || 'unnamed'}${prototype.name ? ` extends ${prototype.name}` : ''}`; throw new Error( 'The class/prototype returned from the extender function must extend the existing structure class/prototype' + - ` (received function ${received}; expected extension of ${structures[structure].name}).` + ` (received function ${received}; expected extension of ${structures[structure].name}).`, ); } diff --git a/src/util/Util.js b/src/util/Util.js index 4ab518773..6e878f34e 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -437,7 +437,7 @@ class Util { return collection.sorted((a, b) => a.rawPosition - b.rawPosition || parseInt(b.id.slice(0, -10)) - parseInt(a.id.slice(0, -10)) || - parseInt(b.id.slice(10)) - parseInt(a.id.slice(10)) + parseInt(b.id.slice(10)) - parseInt(a.id.slice(10)), ); } From c4bda746c893638907e6fa37f3c8a3f3d390ca73 Mon Sep 17 00:00:00 2001 From: Crawl Date: Mon, 24 Feb 2020 23:32:12 +0100 Subject: [PATCH 1328/1359] chore(githooks): husky (#3835) --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 721dc662c..123f1d7b8 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "discord.js-docgen": "discordjs/docgen", "dtslint": "^3.0.0", "eslint": "^6.8.0", + "husky": "^4.2.3", "jest": "^25.1.0", "json-filter-loader": "^1.0.0", "terser-webpack-plugin": "^1.2.2", @@ -120,5 +121,10 @@ "src/client/voice/util/Secretbox.js": false, "src/client/voice/util/Silence.js": false, "src/client/voice/util/VolumeInterface.js": false + }, + "husky": { + "hooks": { + "pre-commit": "npm test" + } } } From 9cb306c823216130e5bddc268ba3aee025a5b98f Mon Sep 17 00:00:00 2001 From: Timo <30553356+y21@users.noreply.github.com> Date: Wed, 26 Feb 2020 12:13:23 +0100 Subject: [PATCH 1329/1359] feat: replace disableEveryone with disableMentions (#3830) * add ClientOptions#disableMentions and MessageOptions#disableMentions * provide tests * don't sanitize controlled mentions * add @here mentions to tests * fix indents (6 spaces instead of 8) * add Util#cleanContent tests * add typings for removeMentions * replace @ with @\u200b AFTER cleaning content as suggested instead of using removeMentions * better explanation of this option * no newline in Util.removeMentions * fix long line * remove double space * remove comments (change has been reverted) * Use Util.removeMentions to remove mentions * use Util.removeMentions in Util.cleanContent --- src/client/Client.js | 4 +- src/structures/APIMessage.js | 14 +++--- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 4 +- src/util/Constants.js | 4 +- src/util/Util.js | 14 ++++-- test/disableMentions.js | 47 +++++++++++++++++++ typings/index.d.ts | 7 +-- 8 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 test/disableMentions.js diff --git a/src/client/Client.js b/src/client/Client.js index ec5128860..e2abbfa77 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -386,8 +386,8 @@ class Client extends BaseClient { if (typeof options.fetchAllMembers !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); } - if (typeof options.disableEveryone !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'disableEveryone', 'a boolean'); + if (typeof options.disableMentions !== 'boolean') { + throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a boolean'); } if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index b05fb68f9..3a52a4608 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -88,6 +88,13 @@ class APIMessage { content = Util.resolveString(this.options.content); } + const disableMentions = typeof this.options.disableMentions === 'undefined' ? + this.target.client.options.disableMentions : + this.options.disableMentions; + if (disableMentions) { + content = Util.removeMentions(content || ''); + } + const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; const isCode = typeof this.options.code !== 'undefined' && this.options.code !== false; const splitOptions = isSplit ? { ...this.options.split } : undefined; @@ -113,13 +120,6 @@ class APIMessage { content = `${mentionPart}${content || ''}`; } - const disableEveryone = typeof this.options.disableEveryone === 'undefined' ? - this.target.client.options.disableEveryone : - this.options.disableEveryone; - if (disableEveryone) { - content = (content || '').replace(/@(everyone|here)/g, '@\u200b$1'); - } - if (isSplit) { content = Util.splitMessage(content || '', splitOptions); } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index b29607040..c76045994 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -85,8 +85,8 @@ class Webhook { * @property {string} [nonce=''] The nonce for the message * @property {Object[]} [embeds] An array of embeds for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here - * should be replaced with plain-text + * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space + * should be placed after every @ character to prevent unexpected mentions * @property {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 0fe0e2dca..38a9dcb31 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -57,8 +57,8 @@ class TextBasedChannel { * @property {string} [content=''] The content for the message * @property {MessageEmbed|Object} [embed] An embed for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableEveryone=this.client.options.disableEveryone] Whether or not @everyone and @here - * should be replaced with plain-text + * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space + * should be placed after every @ character to prevent unexpected mentions * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/util/Constants.js b/src/util/Constants.js index 2bbe580d3..b38e0b580 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,7 +21,7 @@ const browser = exports.browser = typeof window !== 'undefined'; * the message cache lifetime (in seconds, 0 for never) * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) - * @property {boolean} [disableEveryone=false] Default value for {@link MessageOptions#disableEveryone} + * @property {boolean} [disableMentions=false] Default value for {@link MessageOptions#disableMentions} * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when * they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some * important usage information, as partials require you to put checks in place when handling data. @@ -47,7 +47,7 @@ exports.DefaultOptions = { messageCacheLifetime: 0, messageSweepInterval: 0, fetchAllMembers: false, - disableEveryone: false, + disableMentions: false, partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], diff --git a/src/util/Util.js b/src/util/Util.js index 6e878f34e..bc47ada47 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -518,6 +518,15 @@ class Util { return dec; } + /** + * Breaks user, role and everyone/here mentions by adding a zero width space after every @ character + * @param {string} str The string to sanitize + * @returns {string} + */ + static removeMentions(str) { + return str.replace(/@/g, '@\u200b'); + } + /** * The content to have all mentions replaced by the equivalent text. * @param {string} str The string to be converted @@ -525,8 +534,7 @@ class Util { * @returns {string} */ static cleanContent(str, message) { - return str - .replace(/@(everyone|here)/g, '@\u200b$1') + return Util.removeMentions(str .replace(/<@!?[0-9]+>/g, input => { const id = input.replace(/<|!|>|@/g, ''); if (message.channel.type === 'dm') { @@ -550,7 +558,7 @@ class Util { if (message.channel.type === 'dm') return input; const role = message.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; - }); + })); } /** diff --git a/test/disableMentions.js b/test/disableMentions.js new file mode 100644 index 000000000..281633d6e --- /dev/null +++ b/test/disableMentions.js @@ -0,0 +1,47 @@ +const Discord = require('../src'); +const { Util } = Discord; +const { token, prefix } = require('./auth'); + +const client = new Discord.Client({ + // To see a difference, comment out disableMentions and run the same tests using disableEveryone + // You will notice that all messages will mention @everyone + //disableEveryone: true + disableMentions: true +}); + +const tests = [ + // Test 1 + // See https://github.com/discordapp/discord-api-docs/issues/1189 + '@\u202eeveryone @\u202ehere', + + // Test 2 + // See https://github.com/discordapp/discord-api-docs/issues/1241 + // TL;DR: Characters like \u0300 will only be stripped if more than 299 are present + '\u0300@'.repeat(150) + '@\u0300everyone @\u0300here', + + // Test 3 + // Normal @everyone/@here mention + '@everyone @here', +]; + + + +client.on('ready', () => console.log('Ready!')); + +client.on('message', message => { + // Check if message starts with prefix + if (!message.content.startsWith(prefix)) return; + const [command, ...args] = message.content.substr(prefix.length).split(' '); + + // Clean content and log each character + console.log(Util.cleanContent(args.join(' '), message).split('')); + + if (command === 'test1') + message.reply(tests[0]); + else if (command === 'test2') + message.reply(tests[1]); + else if (command === 'test3') + message.reply(tests[2]); +}); + +client.login(token).catch(console.error); \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index 95b0790c7..1b6597402 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1427,6 +1427,7 @@ declare module 'discord.js' { public static basename(path: string, ext?: string): string; public static binaryToID(num: string): Snowflake; public static cleanContent(str: string, message: Message): string; + public static removeMentions(str: string): string; public static cloneObject(obj: object): object; public static convertToBuffer(ab: ArrayBuffer | string): Buffer; public static delayFor(ms: number): Promise; @@ -2068,7 +2069,7 @@ declare module 'discord.js' { messageCacheLifetime?: number; messageSweepInterval?: number; fetchAllMembers?: boolean; - disableEveryone?: boolean; + disableMentions?: boolean; partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; @@ -2464,7 +2465,7 @@ declare module 'discord.js' { nonce?: string; content?: string; embed?: MessageEmbed | MessageEmbedOptions; - disableEveryone?: boolean; + disableMentions?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; @@ -2693,7 +2694,7 @@ declare module 'discord.js' { tts?: boolean; nonce?: string; embeds?: (MessageEmbed | object)[]; - disableEveryone?: boolean; + disableMentions?: boolean; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; From 653784b56445b015a1584de1b693e57fc9069050 Mon Sep 17 00:00:00 2001 From: Amish Shah Date: Wed, 26 Feb 2020 20:00:48 +0000 Subject: [PATCH 1330/1359] chore(StreamDispatcher): remove end event use finish event instead --- src/client/voice/dispatcher/StreamDispatcher.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 20a63fd9f..8901767e5 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -68,8 +68,6 @@ class StreamDispatcher extends Writable { this.on('finish', () => { this._cleanup(); - // Still emitting end for backwards compatibility, probably remove it in the future! - this.emit('end'); this._setSpeaking(0); }); From 6109669c972726a1ee9f0d5ec97a66d3ef3cde60 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 16:41:12 +0000 Subject: [PATCH 1331/1359] typings(WebhookClient): client is not a client (#3838) --- typings/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 1b6597402..97bfb0520 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1614,6 +1614,7 @@ declare module 'discord.js' { public avatar: string; public avatarURL(options?: ImageURLOptions): string | null; public channelID: Snowflake; + public client: Client; public guildID: Snowflake; public name: string; public owner: User | object | null; @@ -1623,6 +1624,7 @@ declare module 'discord.js' { export class WebhookClient extends WebhookMixin(BaseClient) { constructor(id: string, token: string, options?: ClientOptions); + public client: this; public token: string; } @@ -1892,7 +1894,6 @@ declare module 'discord.js' { function WebhookMixin(Base?: Constructable): Constructable; interface WebhookFields { - readonly client: Client; id: Snowflake; readonly createdAt: Date; readonly createdTimestamp: number; From 31a3a86ebccf7db299588e9899d20c19dab80710 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 16:43:11 +0000 Subject: [PATCH 1332/1359] docs(MessageEmbed): document `article` embed type (#3846) --- src/structures/MessageEmbed.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index b5ce08b26..cad60d756 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -14,11 +14,12 @@ class MessageEmbed { setup(data) { // eslint-disable-line complexity /** * The type of this embed, either: + * * `rich` - a rich embed * * `image` - an image embed * * `video` - a video embed * * `gifv` - a gifv embed + * * `article` - an article embed * * `link` - a link embed - * * `rich` - a rich embed * @type {string} */ this.type = data.type; From 261816dcf84d8d871bfe1359eeb2d2e3a28da582 Mon Sep 17 00:00:00 2001 From: Crawl Date: Fri, 28 Feb 2020 17:43:45 +0100 Subject: [PATCH 1333/1359] chore(githooks): commitlint (#3836) --- package.json | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 123f1d7b8..ecfb0c85f 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,8 @@ } }, "devDependencies": { + "@commitlint/cli": "^8.3.5", + "@commitlint/config-angular": "^8.3.4", "@types/node": "^10.12.24", "@types/ws": "^7.2.1", "discord.js-docgen": "discordjs/docgen", @@ -124,7 +126,30 @@ }, "husky": { "hooks": { - "pre-commit": "npm test" + "pre-commit": "npm test", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "commitlint": { + "extends": ["@commitlint/config-angular"], + "rules": { + "type-enum": [ + 2, + "always", + [ + "chore", + "build", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test" + ] + ] } } } From df88729c44135ff9fdf5b3eb79ee1d3e09ff1b02 Mon Sep 17 00:00:00 2001 From: Souji Date: Fri, 28 Feb 2020 17:58:52 +0100 Subject: [PATCH 1334/1359] feat(MessageEmbed): re-introduce MessageEmbed#addField (#3850) * feat(MessageEmbed): re-introduce MessageEmbed#addField * suggestion: sorting alphabetically * suggestion: document inline to default false for #addField --- src/structures/MessageEmbed.js | 13 ++++++++++++- typings/index.d.ts | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index cad60d756..42750c42e 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -215,6 +215,17 @@ class MessageEmbed { (this.footer ? this.footer.text.length : 0)); } + /** + * Adds a field to the embed (max 25). + * @param {StringResolvable} name The name of this field + * @param {StringResolvable} value The value of this field + * @param {boolean} [inline=false] If this field will be displayed inline + * @returns {MessageEmbed} + */ + addField(name, value, inline) { + return this.addFields({ name, value, inline }); + } + /** * Adds fields to the embed (max 25). * @param {...EmbedFieldData|EmbedFieldData[]} fields The fields to add @@ -391,7 +402,7 @@ class MessageEmbed { * @typedef {Object} EmbedFieldData * @property {StringResolvable} name The name of this field * @property {StringResolvable} value The value of this field - * @property {boolean} [inline] If this field will be displayed inline + * @property {boolean} [inline=false] If this field will be displayed inline */ /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 97bfb0520..55dc94eba 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1053,6 +1053,7 @@ declare module 'discord.js' { public type: string; public url?: string; public readonly video: MessageEmbedVideo | null; + public addField(name: StringResolvable, value: StringResolvable, inline?: boolean): this; public addFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): this; public attachFiles(file: (MessageAttachment | FileOptions | string)[]): this; public setAuthor(name: StringResolvable, iconURL?: string, url?: string): this; From 1af1e0cbb8ef2f433054e2d16cb760e503cd5458 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 17:02:51 +0000 Subject: [PATCH 1335/1359] refactor: add some more consistency (#3842) * cleanup(StreamDispatcher): remove old 'end' event * fix(StreamDispatcher): only listen to finish event once * refactor(VoiceWebSocket): use `connection.client` in favour of `connection.voiceManager.client` * fix(VoiceWebSocket): use `client.clearInterval` in favour of `clearInterval` * refactor: destructure EventEmitter * refactor: destructure EventEmitter from events * refactor: use EventEmitter.off in favour of EventEmitter.removeListener * style: order typings alphabetically * oops * fix indent * style: alphabetically organize imports * style: remove extra line * Revert "style: remove extra line" This reverts commit 96e182ed69cfba159ef69aba1d0b218002af67c6. * Revert "style: alphabetically organize imports" This reverts commit 02aee9b06d991731d08d552cf661c5e01343ec6a. * Revert "refactor: destructure EventEmitter from events" This reverts commit 9953b4d267b183e12dee52b284ce7188d67381f6. * Revert "refactor: destructure EventEmitter" This reverts commit 930d7751ab2ee902c8a80559ae9976f67ef6efb0. * Revert "fix(StreamDispatcher): only listen to finish event once" This reverts commit 485a6430a804aba7368e105e9f8bd0c093d7491d. * refactor: use .removeListener instead of .off --- src/client/voice/networking/VoiceWebSocket.js | 6 +- src/client/websocket/WebSocketShard.js | 10 +- src/structures/interfaces/Collector.js | 6 +- typings/index.d.ts | 464 +++++++++--------- 4 files changed, 243 insertions(+), 243 deletions(-) diff --git a/src/client/voice/networking/VoiceWebSocket.js b/src/client/voice/networking/VoiceWebSocket.js index ac6405e14..c6ad45e56 100644 --- a/src/client/voice/networking/VoiceWebSocket.js +++ b/src/client/voice/networking/VoiceWebSocket.js @@ -35,7 +35,7 @@ class VoiceWebSocket extends EventEmitter { * @readonly */ get client() { - return this.connection.voiceManager.client; + return this.connection.client; } shutdown() { @@ -232,7 +232,7 @@ class VoiceWebSocket extends EventEmitter { * @event VoiceWebSocket#warn */ this.emit('warn', 'A voice heartbeat interval is being overwritten'); - clearInterval(this.heartbeatInterval); + this.client.clearInterval(this.heartbeatInterval); } this.heartbeatInterval = this.client.setInterval(this.sendHeartbeat.bind(this), interval); } @@ -245,7 +245,7 @@ class VoiceWebSocket extends EventEmitter { this.emit('warn', 'Tried to clear a heartbeat interval that does not exist'); return; } - clearInterval(this.heartbeatInterval); + this.client.clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } diff --git a/src/client/websocket/WebSocketShard.js b/src/client/websocket/WebSocketShard.js index b313c751a..6fd38a639 100644 --- a/src/client/websocket/WebSocketShard.js +++ b/src/client/websocket/WebSocketShard.js @@ -175,11 +175,11 @@ class WebSocketShard extends EventEmitter { return new Promise((resolve, reject) => { const cleanup = () => { - this.off(ShardEvents.CLOSE, onClose); - this.off(ShardEvents.READY, onReady); - this.off(ShardEvents.RESUMED, onResumed); - this.off(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); - this.off(ShardEvents.DESTROYED, onInvalidOrDestroyed); + this.removeListener(ShardEvents.CLOSE, onClose); + this.removeListener(ShardEvents.READY, onReady); + this.removeListener(ShardEvents.RESUMED, onResumed); + this.removeListener(ShardEvents.INVALID_SESSION, onInvalidOrDestroyed); + this.removeListener(ShardEvents.DESTROYED, onInvalidOrDestroyed); }; const onReady = () => { diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 6231605e7..2ca7fd09a 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -230,8 +230,8 @@ class Collector extends EventEmitter { // eslint-disable-next-line no-await-in-loop await new Promise(resolve => { const tick = () => { - this.off('collect', tick); - this.off('end', tick); + this.removeListener('collect', tick); + this.removeListener('end', tick); return resolve(); }; this.on('collect', tick); @@ -240,7 +240,7 @@ class Collector extends EventEmitter { } } } finally { - this.off('collect', onCollect); + this.removeListener('collect', onCollect); } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 55dc94eba..bed7fa219 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -244,17 +244,6 @@ declare module 'discord.js' { public once(event: string, listener: (...args: any[]) => void): this; } - export class ClientVoiceManager { - constructor(client: Client); - public readonly client: Client; - public connections: Collection; - public broadcasts: VoiceBroadcast[]; - - private joinChannel(channel: VoiceChannel): Promise; - - public createBroadcast(): VoiceBroadcast; - } - export class ClientApplication extends Base { constructor(client: Client, data: object); public botPublic: boolean | null; @@ -275,34 +264,6 @@ declare module 'discord.js' { public toString(): string; } - export class Team extends Base { - constructor(client: Client, data: object); - public id: Snowflake; - public name: string; - public icon: string | null; - public ownerID: Snowflake | null; - public members: Collection; - - public readonly owner: TeamMember; - public readonly createdAt: Date; - public readonly createdTimestamp: number; - - public iconURL(options?: ImageURLOptions): string; - public toJSON(): object; - public toString(): string; - } - - export class TeamMember extends Base { - constructor(team: Team, data: object); - public team: Team; - public readonly id: Snowflake; - public permissions: string[]; - public membershipState: MembershipStates; - public user: User; - - public toString(): string; - } - export class ClientUser extends User { public mfaEnabled: boolean; public verified: boolean; @@ -315,6 +276,17 @@ declare module 'discord.js' { public setUsername(username: string): Promise; } + export class ClientVoiceManager { + constructor(client: Client); + public readonly client: Client; + public connections: Collection; + public broadcasts: VoiceBroadcast[]; + + private joinChannel(channel: VoiceChannel): Promise; + + public createBroadcast(): VoiceBroadcast; + } + export abstract class Collector extends EventEmitter { constructor(client: Client, filter: CollectorFilter, options?: CollectorOptions); private _timeout: NodeJS.Timer | null; @@ -822,18 +794,6 @@ declare module 'discord.js' { public updateOverwrite(userOrRole: RoleResolvable | UserResolvable, options: PermissionOverwriteOption, reason?: string): Promise; } - export class StoreChannel extends GuildChannel { - constructor(guild: Guild, data?: object); - public nsfw: boolean; - } - - export class PartialGroupDMChannel extends Channel { - constructor(client: Client, data: object); - public name: string; - public icon: string | null; - public iconURL(options?: ImageURLOptions): string | null; - } - export class GuildEmoji extends Emoji { constructor(client: Client, data: object, guild: Guild); private _roles: string[]; @@ -889,6 +849,14 @@ declare module 'discord.js' { public valueOf(): string; } + export class HTTPError extends Error { + constructor(message: string, name: string, code: number, method: string, path: string); + public code: number; + public method: string; + public name: string; + public path: string; + } + export class Integration extends Base { constructor(client: Client, data: object, guild: Guild); public account: IntegrationAccount; @@ -908,14 +876,6 @@ declare module 'discord.js' { public sync(): Promise; } - export class HTTPError extends Error { - constructor(message: string, name: string, code: number, method: string, path: string); - public code: number; - public method: string; - public name: string; - public path: string; - } - export class Invite extends Base { constructor(client: Client, data: object); public channel: GuildChannel | PartialGroupDMChannel; @@ -941,11 +901,6 @@ declare module 'discord.js' { public toString(): string; } - export class MessageFlags extends BitField { - public static FLAGS: Record; - public static resolve(bit?: BitFieldResolvable): number; - } - export class Message extends Base { constructor(client: Client, data: object, channel: TextChannel | DMChannel); private _edits: Message[]; @@ -1072,6 +1027,11 @@ declare module 'discord.js' { public static normalizeFields(...fields: EmbedFieldData[] | EmbedFieldData[][]): Required[]; } + export class MessageFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + export class MessageMentions { constructor(message: Message, users: object[] | Collection, roles: Snowflake[] | Collection, everyone: boolean); private _channels: Collection | null; @@ -1114,6 +1074,23 @@ declare module 'discord.js' { public toJSON(): object; } + export class NewsChannel extends TextBasedChannel(GuildChannel) { + constructor(guild: Guild, data?: object); + public messages: MessageManager; + public nsfw: boolean; + public topic: string | null; + public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; + public setNSFW(nsfw: boolean, reason?: string): Promise; + public fetchWebhooks(): Promise>; + } + + export class PartialGroupDMChannel extends Channel { + constructor(client: Client, data: object); + public name: string; + public icon: string | null; + public iconURL(options?: ImageURLOptions): string | null; + } + export class PermissionOverwrites { constructor(guildChannel: GuildChannel, data?: object); public allow: Readonly; @@ -1321,7 +1298,15 @@ declare module 'discord.js' { public static generate(timestamp?: number | Date): Snowflake; } - function VolumeMixin(base: Constructable): Constructable; + export class Speaking extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; + } + + export class StoreChannel extends GuildChannel { + constructor(guild: Guild, data?: object); + public nsfw: boolean; + } class StreamDispatcher extends VolumeMixin(Writable) { constructor(player: object, options?: StreamOptions, streams?: object); @@ -1357,11 +1342,6 @@ declare module 'discord.js' { public once(event: string, listener: (...args: any[]) => void): this; } - export class Speaking extends BitField { - public static FLAGS: Record; - public static resolve(bit?: BitFieldResolvable): number; - } - export class Structures { public static get(structure: K): Extendable[K]; public static get(structure: string): (...args: any[]) => void; @@ -1374,6 +1354,34 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number; } + export class Team extends Base { + constructor(client: Client, data: object); + public id: Snowflake; + public name: string; + public icon: string | null; + public ownerID: Snowflake | null; + public members: Collection; + + public readonly owner: TeamMember; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + + public iconURL(options?: ImageURLOptions): string; + public toJSON(): object; + public toString(): string; + } + + export class TeamMember extends Base { + constructor(team: Team, data: object); + public team: Team; + public readonly id: Snowflake; + public permissions: string[]; + public membershipState: MembershipStates; + public user: User; + + public toString(): string; + } + export class TextChannel extends TextBasedChannel(GuildChannel) { constructor(guild: Guild, data?: object); public messages: MessageManager; @@ -1386,16 +1394,6 @@ declare module 'discord.js' { public fetchWebhooks(): Promise>; } - export class NewsChannel extends TextBasedChannel(GuildChannel) { - constructor(guild: Guild, data?: object); - public messages: MessageManager; - public nsfw: boolean; - public topic: string | null; - public createWebhook(name: string, options?: { avatar?: BufferResolvable | Base64Resolvable; reason?: string; }): Promise; - public setNSFW(nsfw: boolean, reason?: string): Promise; - public fetchWebhooks(): Promise>; - } - export class User extends PartialTextBasedChannel(Base) { constructor(client: Client, data: object); public avatar: string | null; @@ -1749,6 +1747,22 @@ declare module 'discord.js' { public resolveID(resolvable: R): K | null; } + export class GuildChannelManager extends BaseManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + public create(name: string, options: GuildCreateChannelOptions & { type: 'voice'; }): Promise; + public create(name: string, options: GuildCreateChannelOptions & { type: 'category'; }): Promise; + public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text'; }): Promise; + public create(name: string, options: GuildCreateChannelOptions): Promise; + } + + export class GuildEmojiManager extends BaseManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; + public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; + } + export class GuildEmojiRoleManager { constructor(emoji: GuildEmoji); public emoji: GuildEmoji; @@ -1759,26 +1773,20 @@ declare module 'discord.js' { public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection): Promise; } - export class GuildEmojiManager extends BaseManager { - constructor(guild: Guild, iterable?: Iterable); - public guild: Guild; - public create(attachment: BufferResolvable | Base64Resolvable, name: string, options?: GuildEmojiCreateOptions): Promise; - public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; + export class GuildManager extends BaseManager { + constructor(client: Client, iterable?: Iterable); + public create(name: string, options?: { region?: string; icon: BufferResolvable | Base64Resolvable | null; }): Promise; } - export class GuildChannelManager extends BaseManager { + export class GuildMemberManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; - public create(name: string, options: GuildCreateChannelOptions & { type: 'voice'; }): Promise; - public create(name: string, options: GuildCreateChannelOptions & { type: 'category'; }): Promise; - public create(name: string, options?: GuildCreateChannelOptions & { type?: 'text'; }): Promise; - public create(name: string, options: GuildCreateChannelOptions): Promise; - } - - // Hacky workaround because changing the signature of an overridden method errors - class OverridableManager extends BaseManager { - public add(data: any, cache: any): any; - public set(key: any): any; + public ban(user: UserResolvable, options?: BanOptions): Promise; + public fetch(options: UserResolvable | FetchMemberOptions): Promise; + public fetch(options?: FetchMembersOptions): Promise>; + public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; + public prune(options?: GuildPruneMembersOptions): Promise; + public unban(user: UserResolvable, reason?: string): Promise; } export class GuildMemberRoleManager extends OverridableManager { @@ -1794,22 +1802,6 @@ declare module 'discord.js' { public remove(roleOrRoles: RoleResolvable | RoleResolvable[] | Collection, reason?: string): Promise; } - export class GuildMemberManager extends BaseManager { - constructor(guild: Guild, iterable?: Iterable); - public guild: Guild; - public ban(user: UserResolvable, options?: BanOptions): Promise; - public fetch(options: UserResolvable | FetchMemberOptions): Promise; - public fetch(options?: FetchMembersOptions): Promise>; - public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; - public prune(options?: GuildPruneMembersOptions): Promise; - public unban(user: UserResolvable, reason?: string): Promise; - } - - export class GuildManager extends BaseManager { - constructor(client: Client, iterable?: Iterable); - public create(name: string, options?: { region?: string; icon: BufferResolvable | Base64Resolvable | null; }): Promise; - } - export class MessageManager extends BaseManager { constructor(channel: TextChannel | DMChannel, iterable?: Iterable); public channel: TextBasedChannelFields; @@ -1820,6 +1812,12 @@ declare module 'discord.js' { public delete(message: MessageResolvable, reason?: string): Promise; } + // Hacky workaround because changing the signature of an overridden method errors + class OverridableManager extends BaseManager { + public add(data: any, cache: any): any; + public set(key: any): any; + } + export class PresenceManager extends BaseManager { constructor(client: Client, iterable?: Iterable); } @@ -1894,6 +1892,8 @@ declare module 'discord.js' { function WebhookMixin(Base?: Constructable): Constructable; + function VolumeMixin(base: Constructable): Constructable; + interface WebhookFields { id: Snowflake; readonly createdAt: Date; @@ -1932,11 +1932,13 @@ declare module 'discord.js' { | 'WATCHING' | 'CUSTOM_STATUS'; - type MessageFlagsString = 'CROSSPOSTED' - | 'IS_CROSSPOST' - | 'SUPPRESS_EMBEDS' - | 'SOURCE_MESSAGE_DELETED' - | 'URGENT'; + interface AddGuildMemberOptions { + accessToken: string; + nick?: string; + roles?: Collection | RoleResolvable[]; + mute?: boolean; + deaf?: boolean; + } interface APIErrror { UNKNOWN_ACCOUNT: number; @@ -1985,25 +1987,12 @@ declare module 'discord.js' { REACTION_BLOCKED: number; } - interface AddGuildMemberOptions { - accessToken: string; - nick?: string; - roles?: Collection | RoleResolvable[]; - mute?: boolean; - deaf?: boolean; - } - interface AuditLogChange { key: string; old?: any; new?: any; } - interface ImageURLOptions { - format?: ImageExt; - size?: ImageSize; - } - interface AwaitMessagesOptions extends MessageCollectorOptions { errors?: string[]; } @@ -2084,6 +2073,21 @@ declare module 'discord.js' { http?: HTTPOptions; } + type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; + + interface ClientPresenceStatusData { + web?: ClientPresenceStatus; + mobile?: ClientPresenceStatus; + desktop?: ClientPresenceStatus; + } + + interface CloseEvent { + wasClean: boolean; + code: number; + reason: string; + target: WebSocket; + } + type CollectorFilter = (...args: any[]) => boolean; interface CollectorOptions { @@ -2122,6 +2126,13 @@ declare module 'discord.js' { | number | string; + interface CrosspostedChannel { + channelID: Snowflake; + guildID: Snowflake; + type: keyof typeof ChannelType; + name: string; + } + interface DeconstructedSnowflake { timestamp: number; readonly date: Date; @@ -2133,11 +2144,6 @@ declare module 'discord.js' { type DefaultMessageNotifications = 'ALL' | 'MENTIONS'; - interface GuildEmojiEditData { - name?: string; - roles?: Collection | RoleResolvable[]; - } - interface EmbedField { name: string; value: string; @@ -2154,6 +2160,25 @@ declare module 'discord.js' { type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; + interface ErrorEvent { + error: any; + message: string; + type: string; + target: WebSocket; + } + + interface EscapeMarkdownOptions { + codeBlock?: boolean; + inlineCode?: boolean; + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + spoiler?: boolean; + inlineCodeContent?: boolean; + codeBlockContent?: boolean; + } + interface Extendable { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; @@ -2187,17 +2212,6 @@ declare module 'discord.js' { name?: string; } - interface MessageActivity { - partyID: string; - type: number; - } - - interface MessageReference { - channelID: string; - guildID: string; - messageID: string | null; - } - type GuildAuditLogsAction = keyof GuildAuditLogsActions; interface GuildAuditLogsActions { @@ -2286,11 +2300,6 @@ declare module 'discord.js' { name?: string; } - interface GuildEmojiCreateOptions { - roles?: Collection | RoleResolvable[]; - reason?: string; - } - interface GuildEditData { name?: string; region?: string; @@ -2312,6 +2321,16 @@ declare module 'discord.js' { channel: GuildChannelResolvable | null; } + interface GuildEmojiCreateOptions { + roles?: Collection | RoleResolvable[]; + reason?: string; + } + + interface GuildEmojiEditData { + name?: string; + roles?: Collection | RoleResolvable[]; + } + type GuildFeatures = 'ANIMATED_ICON' | 'BANNER' | 'COMMERCE' @@ -2366,6 +2385,11 @@ declare module 'discord.js' { | 1024 | 2048; + interface ImageURLOptions { + format?: ImageExt; + size?: ImageSize; + } + interface IntegrationData { id: string; type: string; @@ -2394,13 +2418,18 @@ declare module 'discord.js' { type MembershipStates = 'INVITED' | 'ACCEPTED'; + type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; + + interface MessageActivity { + partyID: string; + type: number; + } + interface MessageCollectorOptions extends CollectorOptions { max?: number; maxProcessed?: number; } - type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[]; - interface MessageEditOptions { content?: string; embed?: MessageEmbedOptions | null; @@ -2408,6 +2437,26 @@ declare module 'discord.js' { flags?: BitFieldResolvable; } + interface MessageEmbedAuthor { + name?: string; + url?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedFooter { + text?: string; + iconURL?: string; + proxyIconURL?: string; + } + + interface MessageEmbedImage { + url: string; + proxyURL?: string; + height?: number; + width?: number; + } + interface MessageEmbedOptions { title?: string; description?: string; @@ -2423,11 +2472,9 @@ declare module 'discord.js' { footer?: Partial & { icon_url?: string; proxy_icon_url?: string; }; } - interface MessageEmbedAuthor { - name?: string; - url?: string; - iconURL?: string; - proxyIconURL?: string; + interface MessageEmbedProvider { + name: string; + url: string; } interface MessageEmbedThumbnail { @@ -2437,24 +2484,6 @@ declare module 'discord.js' { width?: number; } - interface MessageEmbedFooter { - text?: string; - iconURL?: string; - proxyIconURL?: string; - } - - interface MessageEmbedImage { - url: string; - proxyURL?: string; - height?: number; - width?: number; - } - - interface MessageEmbedProvider { - name: string; - url: string; - } - interface MessageEmbedVideo { url?: string; proxyURL?: string; @@ -2462,6 +2491,18 @@ declare module 'discord.js' { width?: number; } + interface MessageEvent { + data: WebSocket.Data; + type: string; + target: WebSocket; + } + + type MessageFlagsString = 'CROSSPOSTED' + | 'IS_CROSSPOST' + | 'SUPPRESS_EMBEDS' + | 'SOURCE_MESSAGE_DELETED' + | 'URGENT'; + interface MessageOptions { tts?: boolean; nonce?: string; @@ -2476,6 +2517,12 @@ declare module 'discord.js' { type MessageReactionResolvable = MessageReaction | Snowflake; + interface MessageReference { + channelID: string; + guildID: string; + messageID: string | null; + } + type MessageResolvable = Message | Snowflake; type MessageTarget = TextChannel | DMChannel | User | GuildMember | Webhook | WebhookClient; @@ -2513,6 +2560,8 @@ declare module 'discord.js' { interface PermissionOverwriteOption extends Partial> { } + type PermissionResolvable = BitFieldResolvable; + type PermissionString = 'CREATE_INSTANT_INVITE' | 'KICK_MEMBERS' | 'BAN_MEMBERS' @@ -2546,8 +2595,6 @@ declare module 'discord.js' { interface RecursiveArray extends Array> { } - type PermissionResolvable = BitFieldResolvable; - interface PermissionOverwriteOptions { allow: PermissionResolvable; deny: PermissionResolvable; @@ -2569,20 +2616,6 @@ declare module 'discord.js' { type PresenceResolvable = Presence | UserResolvable | Snowflake; - type ClientPresenceStatus = 'online' | 'idle' | 'dnd'; - - interface ClientPresenceStatusData { - web?: ClientPresenceStatus; - mobile?: ClientPresenceStatus; - desktop?: ClientPresenceStatus; - } - - type PartialTypes = 'USER' - | 'CHANNEL' - | 'GUILD_MEMBER' - | 'MESSAGE' - | 'REACTION'; - type Partialize = { id: string; partial: true; @@ -2591,11 +2624,17 @@ declare module 'discord.js' { [K in keyof Omit]: T[K] | null; }; - interface PartialMessage extends Partialize {} interface PartialChannel extends Partialize {} interface PartialGuildMember extends Partialize {} + interface PartialMessage extends Partialize {} interface PartialUser extends Partialize {} + type PartialTypes = 'USER' + | 'CHANNEL' + | 'GUILD_MEMBER' + | 'MESSAGE' + | 'REACTION'; + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; @@ -2746,44 +2785,5 @@ declare module 'discord.js' { | 'VOICE_SERVER_UPDATE' | 'WEBHOOKS_UPDATE'; - interface MessageEvent { - data: WebSocket.Data; - type: string; - target: WebSocket; - } - - interface CloseEvent { - wasClean: boolean; - code: number; - reason: string; - target: WebSocket; - } - - interface ErrorEvent { - error: any; - message: string; - type: string; - target: WebSocket; - } - - interface CrosspostedChannel { - channelID: Snowflake; - guildID: Snowflake; - type: keyof typeof ChannelType; - name: string; - } - - interface EscapeMarkdownOptions { - codeBlock?: boolean; - inlineCode?: boolean; - bold?: boolean; - italic?: boolean; - underline?: boolean; - strikethrough?: boolean; - spoiler?: boolean; - inlineCodeContent?: boolean; - codeBlockContent?: boolean; - } - //#endregion } From 3d0c1df19d66bbb83e2fe73a9377bd6a8428f515 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Fri, 28 Feb 2020 17:26:37 +0000 Subject: [PATCH 1336/1359] =?UTF-8?q?refactor(Guild)/fix(Util):=20use=20re?= =?UTF-8?q?solveID=20and=20regex=20for=20cleanCod=E2=80=A6=20(#3837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(Guild): use resolveID instead of resolve(...).id * fix(Util): use regex for cleanCodeBlockContent --- src/structures/Guild.js | 2 +- src/util/Util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9353f4de5..2368ae626 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -861,7 +861,7 @@ class Guild extends Base { } if (data.afkTimeout) _data.afk_timeout = Number(data.afkTimeout); if (typeof data.icon !== 'undefined') _data.icon = data.icon; - if (data.owner) _data.owner_id = this.client.users.resolve(data.owner).id; + if (data.owner) _data.owner_id = this.client.users.resolveID(data.owner); if (data.splash) _data.splash = data.splash; if (data.banner) _data.banner = data.banner; if (typeof data.explicitContentFilter !== 'undefined') { diff --git a/src/util/Util.js b/src/util/Util.js index bc47ada47..bc2f0f811 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -567,7 +567,7 @@ class Util { * @returns {string} */ static cleanCodeBlockContent(text) { - return text.replace('```', '`\u200b``'); + return text.replace(/```/g, '`\u200b``'); } /** From 2ee0f1cdc69dcd0c8f870bcecc3d36aef0e44ad1 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 29 Feb 2020 06:43:42 +0000 Subject: [PATCH 1337/1359] =?UTF-8?q?feat(GuildManager):=20Allow=20for=20m?= =?UTF-8?q?ore=20options=20for=20GuildManager.cre=E2=80=A6=20(#3742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * typings: add GuildVerificationLevel and GuildExplicitContentFilter * implement new types * fix jsdoc on stores * typo * add more options for GuildStore#create * add channels and roles * update typings * fix typings and use snake case for permissionOverwrites * typings & jsdoc * fix tslint * remove trailing whitespace * fix jsdoc * fix jsdoc * fix oopsies * fix lint * fix lint * fix mr lint man * add typedefs and support for setting channel parents * fix tab indenation * update jsdoc * suggested changes * style: fix silly format * docs(PartialChannelData): name is not optional * style: remove silly format --- src/managers/GuildManager.js | 144 ++++++++++++++++++++----- src/structures/Guild.js | 36 ++++--- src/structures/GuildChannel.js | 4 +- src/structures/PermissionOverwrites.js | 6 +- src/util/Constants.js | 33 ++++-- typings/index.d.ts | 40 +++++-- 6 files changed, 202 insertions(+), 61 deletions(-) diff --git a/src/managers/GuildManager.js b/src/managers/GuildManager.js index 8a28e06b2..407118633 100644 --- a/src/managers/GuildManager.js +++ b/src/managers/GuildManager.js @@ -2,10 +2,17 @@ const BaseManager = require('./BaseManager'); const DataResolver = require('../util/DataResolver'); -const { Events } = require('../util/Constants'); +const { + Events, + VerificationLevels, + DefaultMessageNotifications, + ExplicitContentFilterLevels, +} = require('../util/Constants'); const Guild = require('../structures/Guild'); const GuildChannel = require('../structures/GuildChannel'); const GuildMember = require('../structures/GuildMember'); +const Permissions = require('../util/Permissions'); +const { resolveColor } = require('../util/Util'); const GuildEmoji = require('../structures/GuildEmoji'); const Invite = require('../structures/Invite'); const Role = require('../structures/Role'); @@ -36,6 +43,45 @@ class GuildManager extends BaseManager { * @typedef {Guild|GuildChannel|GuildMember|GuildEmoji|Role|Snowflake|Invite} GuildResolvable */ + /** + * Partial data for a Role. + * @typedef {Object} PartialRoleData + * @property {number} id The ID for this role, used to set channel overrides, + * this is a placeholder and will be replaced by the API after consumption + * @property {string} [name] The name of the role + * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number + * @property {boolean} [hoist] Whether or not the role should be hoisted + * @property {number} [position] The position of the role + * @property {PermissionResolvable|number} [permissions] The permissions of the role + * @property {boolean} [mentionable] Whether or not the role should be mentionable + */ + + /** + * Partial overwrite data. + * @typedef {Object} PartialOverwriteData + * @property {number|Snowflake} id The Role or User ID for this overwrite + * @property {string} [type] The type of this overwrite + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny + */ + + /** + * Partial data for a Channel. + * @typedef {Object} PartialChannelData + * @property {number} [id] The ID for this channel, used to set its parent, + * this is a placeholder and will be replaced by the API after consumption + * @property {number} [parentID] The parent ID for this channel + * @property {string} [type] The type of the channel + * @property {string} name The name of the channel + * @property {string} [topic] The topic of the text channel + * @property {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the channel + * @property {PartialOverwriteData} [permissionOverwrites] + * Overwrites of the channel + * @property {number} [rateLimitPerUser] The rate limit per user of the channel in seconds + */ + /** * Resolves a GuildResolvable to a Guild object. * @method resolve @@ -75,37 +121,81 @@ class GuildManager extends BaseManager { * This is only available to bots in fewer than 10 guilds. * @param {string} name The name of the guild * @param {Object} [options] Options for the creating - * @param {string} [options.region] The region for the server, defaults to the closest one available + * @param {PartialChannelData[]} [options.channels] The channels for this guild + * @param {DefaultMessageNotifications} [options.defaultMessageNotifications] The default message notifications + * for the guild + * @param {ExplicitContentFilterLevel} [options.explicitContentFilter] The explicit content filter level for the guild * @param {BufferResolvable|Base64Resolvable} [options.icon=null] The icon for the guild + * @param {string} [options.region] The region for the server, defaults to the closest one available + * @param {PartialRoleData[]} [options.roles] The roles for this guild, + * the first element of this array is used to change properties of the guild's everyone role. + * @param {VerificationLevel} [options.verificationLevel] The verification level for the guild * @returns {Promise} The guild that was created */ - create(name, { region, icon = null } = {}) { - if (!icon || (typeof icon === 'string' && icon.startsWith('data:'))) { - return new Promise((resolve, reject) => - this.client.api.guilds.post({ data: { name, region, icon } }) - .then(data => { - if (this.client.guilds.cache.has(data.id)) return resolve(this.client.guilds.cache.get(data.id)); - - const handleGuild = guild => { - if (guild.id === data.id) { - this.client.removeListener(Events.GUILD_CREATE, handleGuild); - this.client.clearTimeout(timeout); - resolve(guild); - } - }; - this.client.on(Events.GUILD_CREATE, handleGuild); - - const timeout = this.client.setTimeout(() => { - this.client.removeListener(Events.GUILD_CREATE, handleGuild); - resolve(this.client.guilds.add(data)); - }, 10000); - return undefined; - }, reject), - ); + async create(name, { + channels = [], + defaultMessageNotifications, + explicitContentFilter, + icon = null, + region, + roles = [], + verificationLevel, + } = {}) { + icon = await DataResolver.resolveImage(icon); + if (typeof verificationLevel !== 'undefined' && typeof verificationLevel !== 'number') { + verificationLevel = VerificationLevels.indexOf(verificationLevel); } + if (typeof defaultMessageNotifications !== 'undefined' && typeof defaultMessageNotifications !== 'number') { + defaultMessageNotifications = DefaultMessageNotifications.indexOf(defaultMessageNotifications); + } + if (typeof explicitContentFilter !== 'undefined' && typeof explicitContentFilter !== 'number') { + explicitContentFilter = ExplicitContentFilterLevels.indexOf(explicitContentFilter); + } + for (const channel of channels) { + channel.parent_id = channel.parentID; + delete channel.parentID; + if (!channel.permissionOverwrites) continue; + for (const overwrite of channel.permissionOverwrites) { + if (overwrite.allow) overwrite.allow = Permissions.resolve(overwrite.allow); + if (overwrite.deny) overwrite.deny = Permissions.resolve(overwrite.deny); + } + channel.permission_overwrites = channel.permissionOverwrites; + delete channel.permissionOverwrites; + } + for (const role of roles) { + if (role.color) role.color = resolveColor(role.color); + if (role.permissions) role.permissions = Permissions.resolve(role.permissions); + } + return new Promise((resolve, reject) => + this.client.api.guilds.post({ data: { + name, + region, + icon, + verification_level: verificationLevel, + default_message_notifications: defaultMessageNotifications, + explicit_content_filter: explicitContentFilter, + channels, + roles, + } }) + .then(data => { + if (this.client.guilds.cache.has(data.id)) return resolve(this.client.guilds.cache.get(data.id)); - return DataResolver.resolveImage(icon) - .then(data => this.create(name, { region, icon: data || null })); + const handleGuild = guild => { + if (guild.id === data.id) { + this.client.removeListener(Events.GUILD_CREATE, handleGuild); + this.client.clearTimeout(timeout); + resolve(guild); + } + }; + this.client.on(Events.GUILD_CREATE, handleGuild); + + const timeout = this.client.setTimeout(() => { + this.client.removeListener(Events.GUILD_CREATE, handleGuild); + resolve(this.client.guilds.add(data)); + }, 10000); + return undefined; + }, reject), + ); } } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 2368ae626..57431e42c 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -5,7 +5,13 @@ const Integration = require('./Integration'); const GuildAuditLogs = require('./GuildAuditLogs'); const Webhook = require('./Webhook'); const VoiceRegion = require('./VoiceRegion'); -const { ChannelTypes, DefaultMessageNotifications, PartialTypes } = require('../util/Constants'); +const { + ChannelTypes, + DefaultMessageNotifications, + PartialTypes, + VerificationLevels, + ExplicitContentFilterLevels, +} = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); const DataResolver = require('../util/DataResolver'); @@ -247,15 +253,15 @@ class Guild extends Base { /** * The verification level of the guild - * @type {number} + * @type {VerificationLevel} */ - this.verificationLevel = data.verification_level; + this.verificationLevel = VerificationLevels[data.verification_level]; /** * The explicit content filter level of the guild - * @type {number} + * @type {ExplicitContentFilterLevel} */ - this.explicitContentFilter = data.explicit_content_filter; + this.explicitContentFilter = ExplicitContentFilterLevels[data.explicit_content_filter]; /** * The required MFA level for the guild @@ -821,8 +827,8 @@ class Guild extends Base { * @typedef {Object} GuildEditData * @property {string} [name] The name of the guild * @property {string} [region] The region of the guild - * @property {number} [verificationLevel] The verification level of the guild - * @property {number} [explicitContentFilter] The level of the explicit content filter + * @property {VerificationLevel|number} [verificationLevel] The verification level of the guild + * @property {ExplicitContentFilterLevel|number} [explicitContentFilter] The level of the explicit content filter * @property {ChannelResolvable} [afkChannel] The AFK channel of the guild * @property {ChannelResolvable} [systemChannel] The system channel of the guild * @property {number} [afkTimeout] The AFK timeout of the guild @@ -852,7 +858,11 @@ class Guild extends Base { const _data = {}; if (data.name) _data.name = data.name; if (data.region) _data.region = data.region; - if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel); + if (typeof data.verificationLevel !== 'undefined') { + _data.verification_level = typeof data.verificationLevel === 'number' ? + Number(data.verificationLevel) : + ExplicitContentFilterLevels.indexOf(data.verificationLevel); + } if (typeof data.afkChannel !== 'undefined') { _data.afk_channel_id = this.client.channels.resolveID(data.afkChannel); } @@ -865,12 +875,14 @@ class Guild extends Base { if (data.splash) _data.splash = data.splash; if (data.banner) _data.banner = data.banner; if (typeof data.explicitContentFilter !== 'undefined') { - _data.explicit_content_filter = Number(data.explicitContentFilter); + _data.explicit_content_filter = typeof data.explicitContentFilter === 'number' ? + data.explicitContentFilter : + ExplicitContentFilterLevels.indexOf(data.explicitContentFilter); } if (typeof data.defaultMessageNotifications !== 'undefined') { _data.default_message_notifications = typeof data.defaultMessageNotifications === 'string' ? DefaultMessageNotifications.indexOf(data.defaultMessageNotifications) : - Number(data.defaultMessageNotifications); + data.defaultMessageNotifications; } if (typeof data.systemChannelFlags !== 'undefined') { _data.system_channel_flags = SystemChannelFlags.resolve(data.systemChannelFlags); @@ -881,7 +893,7 @@ class Guild extends Base { /** * Edits the level of the explicit content filter. - * @param {number} explicitContentFilter The new level of the explicit content filter + * @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter * @param {string} [reason] Reason for changing the level of the guild's explicit content filter * @returns {Promise} */ @@ -943,7 +955,7 @@ class Guild extends Base { /** * Edits the verification level of the guild. - * @param {number} verificationLevel The new verification level of the guild + * @param {VerificationLevel|number} verificationLevel The new verification level of the guild * @param {string} [reason] Reason for changing the guild's verification level * @returns {Promise} * @example diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 35a0bc2ab..d80f5b24f 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -211,7 +211,7 @@ class GuildChannel extends Channel { /** * Updates Overwrites for a user or role in this channel. (creates if non-existent) * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} * @example @@ -234,7 +234,7 @@ class GuildChannel extends Channel { /** * Overwrites the permissions for a user or role in this channel. (replaces if existent) * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} * @example diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index cf11eaa94..59a5d93f8 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -56,7 +56,7 @@ class PermissionOverwrites { /** * Updates this permissionOverwrites. - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {string} [reason] Reason for creating/editing this overwrite * @returns {Promise} * @example @@ -99,7 +99,7 @@ class PermissionOverwrites { * 'ATTACH_FILES': false, * } * ``` - * @typedef {Object} PermissionOverwriteOption + * @typedef {Object} PermissionOverwriteOptions */ /** @@ -110,7 +110,7 @@ class PermissionOverwrites { /** * Deletes this Permission Overwrite. - * @param {PermissionOverwriteOption} options The options for the update + * @param {PermissionOverwriteOptions} options The options for the update * @param {Object} initialPermissions The initial permissions * @param {PermissionResolvable} initialPermissions.allow Initial allowed permissions * @param {PermissionResolvable} initialPermissions.deny Initial denied permissions diff --git a/src/util/Constants.js b/src/util/Constants.js index b38e0b580..547ffde1d 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -493,21 +493,34 @@ exports.Colors = { NOT_QUITE_BLACK: 0x23272A, }; +/** + * The value set for the explicit content filter levels for a guild: + * * DISABLED + * * MEMBERS_WITHOUT_ROLES + * * ALL_MEMBERS + * @typedef {string} ExplicitContentFilterLevel + */ +exports.ExplicitContentFilterLevels = [ + 'DISABLED', + 'MEMBERS_WITHOUT_ROLES', + 'ALL_MEMBERS', +]; + /** * The value set for the verification levels for a guild: - * * None - * * Low - * * Medium - * * (╯°□°)╯︵ ┻━┻ - * * ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ + * * NONE + * * LOW + * * MEDIUM + * * HIGH + * * VERY_HIGH * @typedef {string} VerificationLevel */ exports.VerificationLevels = [ - 'None', - 'Low', - 'Medium', - '(╯°□°)╯︵ ┻━┻', - '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻', + 'NONE', + 'LOW', + 'MEDIUM', + 'HIGH', + 'VERY_HIGH', ]; /** diff --git a/typings/index.d.ts b/typings/index.d.ts index bed7fa219..1bc54ae47 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -582,7 +582,9 @@ declare module 'discord.js' { }; MessageTypes: MessageType[]; ActivityTypes: ActivityType[]; + ExplicitContentFilterLevels: ExplicitContentFilterLevel[]; DefaultMessageNotifications: DefaultMessageNotifications[]; + VerificationLevels: VerificationLevel[]; MembershipStates: 'INVITED' | 'ACCEPTED'; }; @@ -647,7 +649,7 @@ declare module 'discord.js' { public embedChannelID: Snowflake | null; public embedEnabled: boolean; public emojis: GuildEmojiManager; - public explicitContentFilter: number; + public explicitContentFilter: ExplicitContentFilterLevel; public features: GuildFeatures[]; public icon: string | null; public id: Snowflake; @@ -681,7 +683,7 @@ declare module 'discord.js' { public systemChannelFlags: Readonly; public systemChannelID: Snowflake | null; public vanityURLCode: string | null; - public verificationLevel: number; + public verificationLevel: VerificationLevel; public readonly verified: boolean; public readonly voice: VoiceState | null; public readonly voiceStates: VoiceStateManager; @@ -713,7 +715,7 @@ declare module 'discord.js' { public setChannelPositions(channelPositions: ChannelPosition[]): Promise; public setDefaultMessageNotifications(defaultMessageNotifications: DefaultMessageNotifications | number, reason?: string): Promise; public setEmbed(embed: GuildEmbedData, reason?: string): Promise; - public setExplicitContentFilter(explicitContentFilter: number, reason?: string): Promise; + public setExplicitContentFilter(explicitContentFilter: ExplicitContentFilterLevel, reason?: string): Promise; public setIcon(icon: Base64Resolvable | null, reason?: string): Promise; public setName(name: string, reason?: string): Promise; public setOwner(owner: GuildMemberResolvable, reason?: string): Promise; @@ -722,7 +724,7 @@ declare module 'discord.js' { public setSplash(splash: Base64Resolvable | null, reason?: string): Promise; public setSystemChannel(systemChannel: ChannelResolvable | null, reason?: string): Promise; public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise; - public setVerificationLevel(verificationLevel: number, reason?: string): Promise; + public setVerificationLevel(verificationLevel: VerificationLevel, reason?: string): Promise; public splashURL(options?: ImageURLOptions): string | null; public toJSON(): object; public toString(): string; @@ -2179,6 +2181,8 @@ declare module 'discord.js' { codeBlockContent?: boolean; } + type ExplicitContentFilterLevel = 'DISABLED' | 'MEMBERS_WITHOUT_ROLES' | 'ALL_MEMBERS'; + interface Extendable { GuildEmoji: typeof GuildEmoji; DMChannel: typeof DMChannel; @@ -2303,8 +2307,8 @@ declare module 'discord.js' { interface GuildEditData { name?: string; region?: string; - verificationLevel?: number; - explicitContentFilter?: number; + verificationLevel?: VerificationLevel; + explicitContentFilter?: ExplicitContentFilterLevel; defaultMessageNotifications?: DefaultMessageNotifications | number; afkChannel?: ChannelResolvable; systemChannel?: ChannelResolvable; @@ -2625,9 +2629,27 @@ declare module 'discord.js' { }; interface PartialChannel extends Partialize {} + + interface PartialChannelData { + id?: number; + name: string; + topic?: string; + type?: ChannelType; + parentID?: number; + permissionOverwrites?: { + id: number | Snowflake; + type?: OverwriteType; + allow?: PermissionResolvable; + deny?: PermissionResolvable; + }[]; + } + interface PartialGuildMember extends Partialize {} interface PartialMessage extends Partialize {} - interface PartialUser extends Partialize {} + + interface PartialRoleData extends RoleData { + id?: number; + } type PartialTypes = 'USER' | 'CHANNEL' @@ -2635,6 +2657,8 @@ declare module 'discord.js' { | 'MESSAGE' | 'REACTION'; + interface PartialUser extends Partialize {} + type PresenceStatus = ClientPresenceStatus | 'offline'; type PresenceStatusData = ClientPresenceStatus | 'invisible'; @@ -2720,6 +2744,8 @@ declare module 'discord.js' { type UserResolvable = User | Snowflake | Message | GuildMember; + type VerificationLevel = 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'; + type VoiceStatus = number; interface WebhookEditData { From de4b4a0939e36c74a19019f5f1d0c47a7934956f Mon Sep 17 00:00:00 2001 From: izexi <43889168+izexi@users.noreply.github.com> Date: Sat, 29 Feb 2020 13:18:37 +0000 Subject: [PATCH 1338/1359] feat(GuildMemberStore): add options.withPresences to fetch() (#3562) * feat: add options.withPresences to fetch() feat: update presences if present on received data typings: add user & withPresences to FetchMembersOptions fix: checking for added options ref: qol changes to return type so that all members are fetched oopsie * fix: use Manager.cache * fix(typings): tslint error Co-authored-by: Crawl --- .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 3 ++ src/managers/GuildMemberManager.js | 34 ++++++++++++++----- typings/index.d.ts | 4 ++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js index 9a0a880a7..59120dcd5 100644 --- a/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js +++ b/src/client/websocket/handlers/GUILD_MEMBERS_CHUNK.js @@ -9,6 +9,9 @@ module.exports = (client, { d: data }) => { const members = new Collection(); for (const member of data.members) members.set(member.user.id, guild.members.add(member)); + if (data.presences) { + for (const presence of data.presences) guild.presences.cache.add(Object.assign(presence, { guild })); + } /** * Emitted whenever a chunk of guild members is received (all members come from the same guild). * @event Client#guildMembersChunk diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index 7fae74c8f..23639df57 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -72,8 +72,10 @@ class GuildMemberManager extends BaseManager { /** * Options used to fetch multiple members from a guild. * @typedef {Object} FetchMembersOptions - * @property {string} [query=''] Limit fetch to members with similar usernames + * @property {UserResolvable|UserResolvable[]} user The user(s) to fetch + * @property {?string} query Limit fetch to members with similar usernames * @property {number} [limit=0] Maximum number of members to request + * @property {boolean} [withPresences=false] Whether or not to include the presences */ /** @@ -98,6 +100,11 @@ class GuildMemberManager extends BaseManager { * .then(console.log) * .catch(console.error); * @example + * // Fetch by an array of users including their presences + * guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true }) + * .then(console.log) + * .catch(console.error); + * @example * // Fetch by query * guild.members.fetch({ query: 'hydra', limit: 1 }) * .then(console.log) @@ -108,8 +115,13 @@ class GuildMemberManager extends BaseManager { const user = this.client.users.resolveID(options); if (user) return this._fetchSingle({ user, cache: true }); if (options.user) { - options.user = this.client.users.resolveID(options.user); - if (options.user) return this._fetchSingle(options); + if (Array.isArray(options.user)) { + options.user = options.user.map(u => this.client.users.resolveID(u)); + return this._fetchMany(options); + } else { + options.user = this.client.users.resolveID(options.user); + } + if (!options.limit && !options.withPresences) return this._fetchSingle(options); } return this._fetchMany(options); } @@ -200,32 +212,38 @@ class GuildMemberManager extends BaseManager { .then(data => this.add(data, cache)); } - _fetchMany({ query = '', limit = 0 } = {}) { + _fetchMany({ limit = 0, withPresences: presences = false, user: user_ids, query } = {}) { return new Promise((resolve, reject) => { - if (this.guild.memberCount === this.cache.size && !query && !limit) { + if (this.guild.memberCount === this.cache.size && (!query && !limit && !presences && !user_ids)) { resolve(this.cache); return; } + if (!query && !user_ids) query = ''; this.guild.shard.send({ op: OPCodes.REQUEST_GUILD_MEMBERS, d: { guild_id: this.guild.id, + presences, + user_ids, query, limit, }, }); const fetchedMembers = new Collection(); + const option = query || limit || presences || user_ids; const handler = (members, guild) => { if (guild.id !== this.guild.id) return; timeout.refresh(); for (const member of members.values()) { - if (query || limit) fetchedMembers.set(member.id, member); + if (option) fetchedMembers.set(member.id, member); } if (this.guild.memberCount <= this.cache.size || - ((query || limit) && members.size < 1000) || + (option && members.size < 1000) || (limit && fetchedMembers.size >= limit)) { this.guild.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler); - resolve(query || limit ? fetchedMembers : this.cache); + let fetched = option ? fetchedMembers : this.cache; + if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first(); + resolve(fetched); } }; const timeout = this.guild.client.setTimeout(() => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 1bc54ae47..42b05a312 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1784,7 +1784,7 @@ declare module 'discord.js' { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; public ban(user: UserResolvable, options?: BanOptions): Promise; - public fetch(options: UserResolvable | FetchMemberOptions): Promise; + public fetch(options: UserResolvable | FetchMemberOptions | (FetchMembersOptions & { user: UserResolvable })): Promise; public fetch(options?: FetchMembersOptions): Promise>; public prune(options: GuildPruneMembersOptions & { dry?: false; count: false; }): Promise; public prune(options?: GuildPruneMembersOptions): Promise; @@ -2207,8 +2207,10 @@ declare module 'discord.js' { } interface FetchMembersOptions { + user?: UserResolvable | UserResolvable[]; query?: string; limit?: number; + withPresences?: boolean; } interface FileOptions { From f95df6f7d7aa59f5b425a1da611deaea2d13b0e8 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 29 Feb 2020 13:18:57 +0000 Subject: [PATCH 1339/1359] fix(Shard): cleanup after settling spawn promise (#3799) * clear sharding ready timeout * fix oops * update typings * commited to the wrong branch * fix(Shard): cleanup after settling the spawn promise Co-authored-by: SpaceEEC --- src/sharding/Shard.js | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/sharding/Shard.js b/src/sharding/Shard.js index 7e016fbee..f394b9746 100644 --- a/src/sharding/Shard.js +++ b/src/sharding/Shard.js @@ -131,10 +131,37 @@ class Shard extends EventEmitter { if (spawnTimeout === -1 || spawnTimeout === Infinity) return this.process || this.worker; await new Promise((resolve, reject) => { - this.once('ready', resolve); - this.once('disconnect', () => reject(new Error('SHARDING_READY_DISCONNECTED', this.id))); - this.once('death', () => reject(new Error('SHARDING_READY_DIED', this.id))); - setTimeout(() => reject(new Error('SHARDING_READY_TIMEOUT', this.id)), spawnTimeout); + const cleanup = () => { + clearTimeout(spawnTimeoutTimer); + this.off('ready', onReady); + this.off('disconnect', onDisconnect); + this.off('death', onDeath); + }; + + const onReady = () => { + cleanup(); + resolve(); + }; + + const onDisconnect = () => { + cleanup(); + reject(new Error('SHARDING_READY_DISCONNECTED', this.id)); + }; + + const onDeath = () => { + cleanup(); + reject(new Error('SHARDING_READY_DIED', this.id)); + }; + + const onTimeout = () => { + cleanup(); + reject(new Error('SHARDING_READY_TIMEOUT', this.id)); + }; + + const spawnTimeoutTimer = setTimeout(onTimeout, spawnTimeout); + this.once('ready', onReady); + this.once('disconnect', onDisconnect); + this.once('death', onDeath); }); return this.process || this.worker; } From e4f567c65ef3dfd1f0ae7ddc40a862445549f9d4 Mon Sep 17 00:00:00 2001 From: Sugden <28943913+NotSugden@users.noreply.github.com> Date: Sat, 29 Feb 2020 13:19:21 +0000 Subject: [PATCH 1340/1359] =?UTF-8?q?refactor(GuildChannel):=20change=20ov?= =?UTF-8?q?erwritePermisions=20to=20accept=20an=E2=80=A6=20(#3853)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(GuildChannel): change overwritePermisions to no longer accept an object * fix: check for instanceof Collection too --- src/structures/GuildChannel.js | 24 ++++++++++++++---------- typings/index.d.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index d80f5b24f..349dcad10 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -188,24 +188,28 @@ class GuildChannel extends Channel { /** * Replaces the permission overwrites in this channel. - * @param {Object} [options] Options - * @param {OverwriteResolvable[]|Collection} [options.permissionOverwrites] + * @param {OverwriteResolvable[]|Collection} overwrites * Permission overwrites the channel gets updated with - * @param {string} [options.reason] Reason for updating the channel overwrites + * @param {string} [reason] Reason for updating the channel overwrites * @returns {Promise} * @example - * channel.overwritePermissions({ - * permissionOverwrites: [ + * channel.overwritePermissions([ * { * id: message.author.id, * deny: ['VIEW_CHANNEL'], * }, - * ], - * reason: 'Needed to change permissions' - * }); + * ], 'Needed to change permissions'); */ - overwritePermissions(options = {}) { - return this.edit(options).then(() => this); + overwritePermissions(overwrites, reason) { + if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) { + return Promise.reject(new TypeError( + 'INVALID_TYPE', + 'overwrites', + 'Array or Collection of Permission Overwrites', + true, + )); + } + return this.edit({ permissionOverwrites: overwrites, reason }).then(() => this); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 42b05a312..659a1b89c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -787,7 +787,7 @@ declare module 'discord.js' { public equals(channel: GuildChannel): boolean; public fetchInvites(): Promise>; public lockPermissions(): Promise; - public overwritePermissions(options?: { permissionOverwrites?: OverwriteResolvable[] | Collection, reason?: string; }): Promise; + public overwritePermissions(overwrites: OverwriteResolvable[] | Collection, reason?: string): Promise; public permissionsFor(memberOrRole: GuildMemberResolvable | RoleResolvable): Readonly | null; public setName(name: string, reason?: string): Promise; public setParent(channel: GuildChannel | Snowflake, options?: { lockPermissions?: boolean; reason?: string; }): Promise; From bbe169deac64a65f283f76b65e56c59808584f82 Mon Sep 17 00:00:00 2001 From: Bence <56838314+1s3k3b@users.noreply.github.com> Date: Sat, 29 Feb 2020 14:19:56 +0100 Subject: [PATCH 1341/1359] fix(MessageEmbed): prevent possible destructuring error --- src/structures/MessageEmbed.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 42750c42e..35e2ec2bf 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -411,7 +411,13 @@ class MessageEmbed { * @returns {EmbedField[]} */ static normalizeFields(...fields) { - return fields.flat(2).map(({ name, value, inline }) => this.normalizeField(name, value, inline)); + return fields.flat(2).map(field => + this.normalizeField( + field && field.name, + field && field.value, + field && typeof field.inline === 'boolean' ? field.inline : false, + ), + ); } } From 9c8aaf1bbcc220299e65cadb6ac54e1a7f208576 Mon Sep 17 00:00:00 2001 From: Papa Date: Sat, 29 Feb 2020 06:20:39 -0700 Subject: [PATCH 1342/1359] feat: reimplement disableEveryone into disableMentions * User input sanitation: reimplement disableEveryone into disableMentions * Change default value of ClientOptions#disableMentions to 'none' * Update type declarations of disableMentions to include default * update for compliance with ESLint * Overlooked these files. Updated for complete compliance with ESLint --- src/client/Client.js | 4 +- src/structures/APIMessage.js | 10 ++++- src/structures/Webhook.js | 4 +- src/structures/interfaces/TextBasedChannel.js | 4 +- src/util/Constants.js | 5 ++- src/util/Util.js | 43 ++++++++++++------- test/disableMentions.js | 4 +- typings/index.d.ts | 6 +-- 8 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/client/Client.js b/src/client/Client.js index e2abbfa77..a23e5a691 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -386,8 +386,8 @@ class Client extends BaseClient { if (typeof options.fetchAllMembers !== 'boolean') { throw new TypeError('CLIENT_INVALID_OPTION', 'fetchAllMembers', 'a boolean'); } - if (typeof options.disableMentions !== 'boolean') { - throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a boolean'); + if (typeof options.disableMentions !== 'string') { + throw new TypeError('CLIENT_INVALID_OPTION', 'disableMentions', 'a string'); } if (!Array.isArray(options.partials)) { throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array'); diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 3a52a4608..385640a89 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -91,8 +91,16 @@ class APIMessage { const disableMentions = typeof this.options.disableMentions === 'undefined' ? this.target.client.options.disableMentions : this.options.disableMentions; - if (disableMentions) { + if (disableMentions === 'all') { content = Util.removeMentions(content || ''); + } else if (disableMentions === 'everyone') { + content = (content || '').replace(/@([^<>@ ]*)/gsmu, (match, target) => { + if (target.match(/^[&!]?\d+$/)) { + return `@${target}`; + } else { + return `@\u200b${target}`; + } + }); } const isSplit = typeof this.options.split !== 'undefined' && this.options.split !== false; diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index c76045994..6a5be72bf 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -85,8 +85,8 @@ class Webhook { * @property {string} [nonce=''] The nonce for the message * @property {Object[]} [embeds] An array of embeds for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space - * should be placed after every @ character to prevent unexpected mentions + * @property {'none' | 'all' | 'everyone'} [disableMentions=this.client.options.disableMentions] Whether or not + * all mentions or everyone/here mentions should be sanitized to prevent unexpected mentions * @property {FileOptions[]|string[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 38a9dcb31..b2abf50af 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -57,8 +57,8 @@ class TextBasedChannel { * @property {string} [content=''] The content for the message * @property {MessageEmbed|Object} [embed] An embed for the message * (see [here](https://discordapp.com/developers/docs/resources/channel#embed-object) for more details) - * @property {boolean} [disableMentions=this.client.options.disableMentions] Whether or not a zero width space - * should be placed after every @ character to prevent unexpected mentions + * @property {'none' | 'all' | 'everyone'} [disableMentions=this.client.options.disableMentions] Whether or not + * all mentionsor everyone/here mentions should be sanitized to prevent unexpected mentions * @property {FileOptions[]|BufferResolvable[]} [files] Files to send with the message * @property {string|boolean} [code] Language for optional codeblock formatting to apply * @property {boolean|SplitOptions} [split=false] Whether or not the message should be split into multiple messages if diff --git a/src/util/Constants.js b/src/util/Constants.js index 547ffde1d..604a13f9e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -21,7 +21,8 @@ const browser = exports.browser = typeof window !== 'undefined'; * the message cache lifetime (in seconds, 0 for never) * @property {boolean} [fetchAllMembers=false] Whether to cache all guild members and users upon startup, as well as * upon joining a guild (should be avoided whenever possible) - * @property {boolean} [disableMentions=false] Default value for {@link MessageOptions#disableMentions} + * @property {'none' | 'all' | 'everyone'} [disableMentions='none'] Default value + * for {@link MessageOptions#disableMentions} * @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when * they're missing all the data for a particular structure. See the "Partials" topic listed in the sidebar for some * important usage information, as partials require you to put checks in place when handling data. @@ -47,7 +48,7 @@ exports.DefaultOptions = { messageCacheLifetime: 0, messageSweepInterval: 0, fetchAllMembers: false, - disableMentions: false, + disableMentions: 'none', partials: [], restWsBridgeTimeout: 5000, disabledEvents: [], diff --git a/src/util/Util.js b/src/util/Util.js index bc2f0f811..a7c2a23d1 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -534,22 +534,30 @@ class Util { * @returns {string} */ static cleanContent(str, message) { - return Util.removeMentions(str - .replace(/<@!?[0-9]+>/g, input => { - const id = input.replace(/<|!|>|@/g, ''); - if (message.channel.type === 'dm') { - const user = message.client.users.cache.get(id); - return user ? `@${user.username}` : input; - } - - const member = message.channel.guild.members.cache.get(id); - if (member) { - return `@${member.displayName}`; + if (message.client.options.disableMentions === 'everyone') { + str = str.replace(/@([^<>@ ]*)/gsmu, (match, target) => { + if (target.match(/^[&!]?\d+$/)) { + return `@${target}`; } else { - const user = message.client.users.cache.get(id); - return user ? `@${user.username}` : input; + return `@\u200b${target}`; } - }) + }); + } + str = str.replace(/<@!?[0-9]+>/g, input => { + const id = input.replace(/<|!|>|@/g, ''); + if (message.channel.type === 'dm') { + const user = message.client.users.cache.get(id); + return user ? `@${user.username}` : input; + } + + const member = message.channel.guild.members.cache.get(id); + if (member) { + return `@${member.displayName}`; + } else { + const user = message.client.users.cache.get(id); + return user ? `@${user.username}` : input; + } + }) .replace(/<#[0-9]+>/g, input => { const channel = message.client.channels.cache.get(input.replace(/<|#|>/g, '')); return channel ? `#${channel.name}` : input; @@ -558,7 +566,12 @@ class Util { if (message.channel.type === 'dm') return input; const role = message.guild.roles.cache.get(input.replace(/<|@|>|&/g, '')); return role ? `@${role.name}` : input; - })); + }); + if (message.client.options.disableMentions === 'all') { + return Util.removeMentions(str); + } else { + return str; + } } /** diff --git a/test/disableMentions.js b/test/disableMentions.js index 281633d6e..f227a0b49 100644 --- a/test/disableMentions.js +++ b/test/disableMentions.js @@ -6,7 +6,7 @@ const client = new Discord.Client({ // To see a difference, comment out disableMentions and run the same tests using disableEveryone // You will notice that all messages will mention @everyone //disableEveryone: true - disableMentions: true + disableMentions: 'everyone' }); const tests = [ @@ -44,4 +44,4 @@ client.on('message', message => { message.reply(tests[2]); }); -client.login(token).catch(console.error); \ No newline at end of file +client.login(token).catch(console.error); diff --git a/typings/index.d.ts b/typings/index.d.ts index 659a1b89c..cac46ed04 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2062,7 +2062,7 @@ declare module 'discord.js' { messageCacheLifetime?: number; messageSweepInterval?: number; fetchAllMembers?: boolean; - disableMentions?: boolean; + disableMentions?: 'none' | 'all' | 'everyone'; partials?: PartialTypes[]; restWsBridgeTimeout?: number; restTimeOffset?: number; @@ -2514,7 +2514,7 @@ declare module 'discord.js' { nonce?: string; content?: string; embed?: MessageEmbed | MessageEmbedOptions; - disableMentions?: boolean; + disableMentions?: 'none' | 'all' | 'everyone'; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; @@ -2763,7 +2763,7 @@ declare module 'discord.js' { tts?: boolean; nonce?: string; embeds?: (MessageEmbed | object)[]; - disableMentions?: boolean; + disableMentions?: 'none' | 'all' | 'everyone'; files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; code?: string | boolean; split?: boolean | SplitOptions; From 6650d50a56c42aca9657b627d2333349bd05b181 Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Sun, 1 Mar 2020 00:21:43 +1100 Subject: [PATCH 1343/1359] =?UTF-8?q?feat(MessageEmbed):=20Support=20Embed?= =?UTF-8?q?FieldData[]=20instead=20of=20EmbedFi=E2=80=A6=20(#3845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(typings): MessageEmbedOptions#fields should be EmbedFieldData[] * feat(MessageEmbed): Allow optional EmbedFieldData#inline in constructor * docs(MessageEmbed): revert type change of fields Co-authored-by: Crawl Co-authored-by: SpaceEEC --- src/structures/MessageEmbed.js | 2 +- typings/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 35e2ec2bf..905433e2d 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -65,7 +65,7 @@ class MessageEmbed { * The fields of this embed * @type {EmbedField[]} */ - this.fields = data.fields ? data.fields.map(Util.cloneObject) : []; + this.fields = data.fields ? this.constructor.normalizeFields(data.fields) : []; /** * @typedef {Object} MessageEmbedThumbnail diff --git a/typings/index.d.ts b/typings/index.d.ts index cac46ed04..ad02dc090 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2469,7 +2469,7 @@ declare module 'discord.js' { url?: string; timestamp?: Date | number; color?: ColorResolvable; - fields?: EmbedField[]; + fields?: EmbedFieldData[]; files?: (MessageAttachment | string | FileOptions)[]; author?: Partial & { icon_url?: string; proxy_icon_url?: string; }; thumbnail?: Partial & { proxy_url?: string; }; From c065156a8883d81e58f3603e468289344f8a20a5 Mon Sep 17 00:00:00 2001 From: Crawl Date: Sat, 29 Feb 2020 14:35:57 +0100 Subject: [PATCH 1344/1359] chore: consistency/prettier (#3852) * chore: consistency/prettier * chore: rebase * chore: rebase * chore: include typings * fix: include typings file in prettier lint-staged --- .eslintrc.json | 87 +- .github/CONTRIBUTING.md | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 8 +- .github/ISSUE_TEMPLATE/feature_request.md | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 5 +- .tern-project | 8 +- README.md | 42 +- docs/examples/attachments.md | 70 +- docs/examples/embed.js | 4 +- docs/examples/moderation.md | 68 +- docs/general/faq.md | 6 +- docs/general/updating.md | 78 +- docs/general/welcome.md | 32 +- docs/topics/partials.md | 2 +- docs/topics/voice.md | 28 +- docs/topics/web.md | 7 + jsdoc.json | 2 +- package.json | 16 +- src/client/BaseClient.js | 2 +- src/client/Client.js | 74 +- src/client/WebhookClient.js | 2 +- src/client/actions/Action.js | 65 +- src/client/actions/ChannelDelete.js | 2 +- src/client/actions/GuildIntegrationsUpdate.js | 1 - src/client/actions/GuildMemberRemove.js | 1 - src/client/actions/GuildRoleCreate.js | 1 - src/client/actions/GuildRoleDelete.js | 1 - src/client/actions/GuildRoleUpdate.js | 1 - src/client/actions/GuildUpdate.js | 1 - src/client/actions/MessageCreate.js | 1 - src/client/actions/MessageDelete.js | 1 - src/client/actions/MessageDeleteBulk.js | 13 +- src/client/actions/MessageReactionRemove.js | 1 - src/client/actions/VoiceStateUpdate.js | 9 +- src/client/actions/WebhooksUpdate.js | 1 - src/client/voice/ClientVoiceManager.js | 7 +- src/client/voice/VoiceBroadcast.js | 7 +- src/client/voice/VoiceConnection.js | 72 +- .../voice/dispatcher/StreamDispatcher.js | 38 +- src/client/voice/networking/VoiceUDPClient.js | 4 +- src/client/voice/networking/VoiceWebSocket.js | 7 +- src/client/voice/player/BasePlayer.js | 14 +- .../voice/player/BroadcastAudioPlayer.js | 4 +- src/client/voice/receiver/PacketHandler.js | 8 +- src/client/voice/util/PlayInterface.js | 4 +- src/client/voice/util/Silence.js | 2 +- src/client/voice/util/VolumeInterface.js | 13 +- src/client/websocket/WebSocketManager.js | 10 +- src/client/websocket/WebSocketShard.js | 26 +- .../websocket/handlers/CHANNEL_UPDATE.js | 1 - src/client/websocket/handlers/GUILD_CREATE.js | 12 +- .../websocket/handlers/GUILD_MEMBERS_CHUNK.js | 2 +- .../websocket/handlers/GUILD_MEMBER_ADD.js | 10 +- src/client/websocket/handlers/TYPING_START.js | 12 +- src/errors/Messages.js | 16 +- src/managers/BaseManager.js | 6 +- src/managers/ChannelManager.js | 10 +- src/managers/GuildChannelManager.js | 2 +- src/managers/GuildEmojiManager.js | 21 +- src/managers/GuildEmojiRoleManager.js | 8 +- src/managers/GuildManager.js | 74 +- src/managers/GuildMemberManager.js | 41 +- src/managers/GuildMemberRoleManager.js | 20 +- src/managers/MessageManager.js | 140 +- src/managers/PresenceManager.js | 26 +- src/managers/ReactionManager.js | 40 +- src/managers/ReactionUserManager.js | 13 +- src/managers/RoleManager.js | 21 +- src/managers/UserManager.js | 2 +- src/rest/APIRequest.js | 15 +- src/rest/APIRouter.js | 24 +- src/rest/DiscordAPIError.js | 2 +- src/rest/RESTManager.js | 18 +- src/rest/RequestHandler.js | 14 +- src/sharding/Shard.js | 16 +- src/sharding/ShardClientUtil.js | 41 +- src/sharding/ShardingManager.js | 39 +- src/structures/APIMessage.js | 22 +- src/structures/Base.js | 4 +- src/structures/Channel.js | 7 +- src/structures/ClientApplication.js | 30 +- src/structures/ClientPresence.js | 48 +- src/structures/ClientUser.js | 6 +- src/structures/Emoji.js | 3 +- src/structures/Guild.js | 277 +- src/structures/GuildAuditLogs.js | 174 +- src/structures/GuildChannel.js | 137 +- src/structures/GuildEmoji.js | 32 +- src/structures/GuildMember.js | 32 +- src/structures/Integration.js | 15 +- src/structures/Invite.js | 10 +- src/structures/Message.js | 136 +- src/structures/MessageEmbed.js | 138 +- src/structures/MessageMentions.js | 4 +- src/structures/MessageReaction.js | 7 +- src/structures/PermissionOverwrites.js | 19 +- src/structures/Presence.js | 51 +- src/structures/ReactionCollector.js | 3 +- src/structures/ReactionEmoji.js | 2 +- src/structures/Role.js | 82 +- src/structures/Team.js | 4 +- src/structures/TextChannel.js | 14 +- src/structures/User.js | 34 +- src/structures/VoiceChannel.js | 4 +- src/structures/VoiceState.js | 12 +- src/structures/Webhook.js | 54 +- src/structures/interfaces/Collector.js | 2 +- src/structures/interfaces/TextBasedChannel.js | 50 +- src/util/Collection.js | 2 +- src/util/Constants.js | 105 +- src/util/DataResolver.js | 6 +- src/util/Snowflake.js | 12 +- src/util/Structures.js | 2 +- src/util/Util.js | 148 +- test/disableMentions.js | 47 +- test/escapeMarkdown.test.js | 147 +- test/random.js | 121 +- test/sendtest.js | 81 +- test/shard.js | 14 +- test/tester1000.js | 9 +- test/voice.js | 36 +- test/webhooktest.js | 95 +- test/webpack.html | 47 +- tsconfig.json | 32 +- tslint.json | 54 +- typings/index.d.ts | 5828 +++++++++-------- webpack.config.js | 6 +- 127 files changed, 5198 insertions(+), 4513 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 549500347..f524f5f7d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,6 @@ { - "extends": "eslint:recommended", + "extends": ["eslint:recommended", "plugin:prettier/recommended"], + "plugins": ["import"], "parserOptions": { "ecmaVersion": 2019 }, @@ -7,42 +8,58 @@ "es6": true, "node": true }, - "overrides": [ - { "files": ["*.browser.js"], "env": { "browser": true } } - ], + "overrides": [{ "files": ["*.browser.js"], "env": { "browser": true } }], "rules": { + "import/order": [ + "error", + { + "groups": ["builtin", "external", "internal", "index", "sibling", "parent"], + "alphabetize": { + "order": "asc" + } + } + ], + "prettier/prettier": [ + 2, + { + "printWidth": 120, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "all", + "endOfLine": "lf" + } + ], "strict": ["error", "global"], "no-await-in-loop": "warn", "no-compare-neg-zero": "error", - "no-extra-parens": ["warn", "all", { - "nestedBinaryExpressions": false - }], "no-template-curly-in-string": "error", "no-unsafe-negation": "error", - "valid-jsdoc": ["error", { - "requireReturn": false, - "requireReturnDescription": false, - "prefer": { - "return": "returns", - "arg": "param" - }, - "preferType": { - "String": "string", - "Number": "number", - "Boolean": "boolean", - "Symbol": "symbol", - "object": "Object", - "function": "Function", - "array": "Array", - "date": "Date", - "error": "Error", - "null": "void" + "valid-jsdoc": [ + "error", + { + "requireReturn": false, + "requireReturnDescription": false, + "prefer": { + "return": "returns", + "arg": "param" + }, + "preferType": { + "String": "string", + "Number": "number", + "Boolean": "boolean", + "Symbol": "symbol", + "object": "Object", + "function": "Function", + "array": "Array", + "date": "Date", + "error": "Error", + "null": "void" + } } - }], + ], "accessor-pairs": "warn", "array-callback-return": "error", - "complexity": "warn", "consistent-return": "error", "curly": ["error", "multi-line", "consistent"], "dot-location": ["error", "property"], @@ -100,7 +117,6 @@ "func-names": "error", "func-name-matching": "error", "func-style": ["error", "declaration", { "allowArrowFunctions": true }], - "indent": ["error", 2, { "SwitchCase": 1 }], "key-spacing": "error", "keyword-spacing": "error", "max-depth": "error", @@ -112,7 +128,6 @@ "no-array-constructor": "error", "no-inline-comments": "error", "no-lonely-if": "error", - "no-mixed-operators": "error", "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], "no-new-object": "error", "no-spaced-func": "error", @@ -122,18 +137,20 @@ "nonblock-statement-body-position": "error", "object-curly-spacing": ["error", "always"], "operator-assignment": "error", - "operator-linebreak": ["error", "after"], "padded-blocks": ["error", "never"], "quote-props": ["error", "as-needed"], "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], "semi-spacing": "error", "semi": "error", "space-before-blocks": "error", - "space-before-function-paren": ["error", { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - }], + "space-before-function-paren": [ + "error", + { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + } + ], "space-in-parens": "error", "space-infix-ops": "error", "space-unary-ops": "error", diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fd6de7242..84ecd2d46 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,6 +7,7 @@ pull request. We use ESLint to enforce a consistent coding style, so having that is a great boon to your development process. ## Setup + To get ready to work on the codebase, please do the following: 1. Fork & clone the repository, and make sure you're on the **master** branch diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d5061af2b..caa9425b7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,12 +1,11 @@ --- + name: Bug report about: Report incorrect or unexpected behaviour of discord.js title: '' labels: 's: unverified, type: bug' assignees: '' - --- -