From f7664b01a2d01e669f661ad36d8fcbe32d29be0d Mon Sep 17 00:00:00 2001 From: SpaceEEC Date: Mon, 21 Aug 2017 22:25:21 +0200 Subject: [PATCH] Backports (#1813) * Backported OAuth2Application https://github.com/hydrabolt/discord.js/commit/201ecd25a2e017c0be74ed2c68abc2c323813a87 * Backported retry on 500 https://github.com/hydrabolt/discord.js/commit/57b69803132355b506f9919f37a55915ccdf86d3 * Backported https://github.com/hydrabolt/discord.js/commit/b8034525e3da2a5bd05fab44841ee3b0149ff70c and https://github.com/hydrabolt/discord.js/commit/fa5c4efa2bc545bc4df6f449dde343a6dcfdcfe2 --- src/client/rest/RESTMethods.js | 7 +- src/client/rest/RequestHandlers/Burst.js | 6 ++ src/client/rest/RequestHandlers/Sequential.js | 3 + .../packets/WebSocketPacketManager.js | 1 + .../websocket/packets/handlers/Ready.js | 1 + .../handlers/UserGuildSettingsUpdate.js | 18 +++++ src/structures/ClientUser.js | 13 ++++ src/structures/ClientUserChannelOverride.js | 28 +++++++ src/structures/ClientUserGuildSettings.js | 58 ++++++++++++++ src/structures/Guild.js | 61 +++++++++++++++ src/structures/GuildChannel.js | 31 ++++++++ src/structures/OAuth2Application.js | 3 +- src/structures/User.js | 2 +- src/util/Constants.js | 77 ++++++++++++++++++- 14 files changed, 304 insertions(+), 5 deletions(-) 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/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 023a50a1b..14a28ee5e 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -878,7 +878,8 @@ class RESTMethods { } resetApplication(id) { - return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).reset, true) + return this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetToken, true) + .then(() => this.rest.makeRequest('post', Endpoints.OAUTH2.Application(id).resetSecret, true)) .then(app => new OAuth2Application(this.client, app)); } @@ -908,6 +909,10 @@ class RESTMethods { patchUserSettings(data) { return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data); } + + patchClientUserGuildSettings(guildID, data) { + return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').Guild(guildID).settings, true, data); + } } module.exports = RESTMethods; diff --git a/src/client/rest/RequestHandlers/Burst.js b/src/client/rest/RequestHandlers/Burst.js index e5a160246..f3311ab65 100644 --- a/src/client/rest/RequestHandlers/Burst.js +++ b/src/client/rest/RequestHandlers/Burst.js @@ -40,6 +40,12 @@ class BurstRequestHandler extends RequestHandler { this.handle(); this.resetTimeout = null; }, Number(res.headers['retry-after']) + this.client.options.restTimeOffset); + } else if (err.status >= 500 && err.status < 600) { + this.queue.unshift(item); + this.resetTimeout = this.client.setTimeout(() => { + this.handle(); + this.resetTimeout = null; + }, 1e3 + this.client.options.restTimeOffset); } else { item.reject(err.status === 400 ? new DiscordAPIError(res.body) : err); this.handle(); diff --git a/src/client/rest/RequestHandlers/Sequential.js b/src/client/rest/RequestHandlers/Sequential.js index 04f805237..42b07452e 100644 --- a/src/client/rest/RequestHandlers/Sequential.js +++ b/src/client/rest/RequestHandlers/Sequential.js @@ -64,6 +64,9 @@ class SequentialRequestHandler extends RequestHandler { resolve(); }, Number(res.headers['retry-after']) + this.restManager.client.options.restTimeOffset); if (res.headers['x-ratelimit-global']) this.globalLimit = true; + } else if (err.status >= 500 && err.status < 600) { + this.queue.unshift(item); + this.restManager.client.setTimeout(resolve, 1e3 + this.client.options.restTimeOffset); } else { item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.body) : err); resolve(err); 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..1470a3c84 --- /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 e6171ebec..a612e1a2e 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'); /** @@ -73,6 +74,18 @@ class ClientUser extends User { * @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(settings, this.client)); + } + } } edit(data) { diff --git a/src/structures/ClientUserChannelOverride.js b/src/structures/ClientUserChannelOverride.js new file mode 100644 index 000000000..12790b9ac --- /dev/null +++ b/src/structures/ClientUserChannelOverride.js @@ -0,0 +1,28 @@ +const Constants = 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 + */ + 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..146be9872 --- /dev/null +++ b/src/structures/ClientUserGuildSettings.js @@ -0,0 +1,58 @@ +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, client) { + /** + * 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 this 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 + */ + 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(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.client.rest.methods.patchClientUserGuildSettings(this.guildID, { [name]: value }); + } +} + +module.exports = ClientUserGuildSettings; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index baecfc7c3..eb7d0f7ff 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -330,6 +330,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; @@ -337,6 +338,66 @@ class Guild { 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 + * This is only available when using a user account. + * @type {?MessageNotificationType} + * @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} diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 9b3e83c0e..2c728afbe 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -3,6 +3,7 @@ const Role = require('./Role'); const PermissionOverwrites = require('./PermissionOverwrites'); const Permissions = require('../util/Permissions'); const Collection = require('../util/Collection'); +const Constants = require('../util/Constants'); /** * Represents a guild channel (i.e. text channels and voice channels). @@ -328,6 +329,36 @@ class GuildChannel extends Channel { this.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_CHANNELS); } + /** + * 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 + * This is only available when using a user account. + * @type {?MessageNotificationType} + * @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 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/structures/OAuth2Application.js b/src/structures/OAuth2Application.js index 0e037ab7d..cfd657d95 100644 --- a/src/structures/OAuth2Application.js +++ b/src/structures/OAuth2Application.js @@ -37,7 +37,7 @@ class OAuth2Application { /** * The app's icon hash - * @type {string} + * @type {?string} */ this.icon = data.icon; @@ -124,6 +124,7 @@ class OAuth2Application { /** * Reset the app's secret and bot token. + * This is only available when using a user account. * @returns {OAuth2Application} */ reset() { diff --git a/src/structures/User.js b/src/structures/User.js index 94344e4dd..027eedbb6 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -12,7 +12,7 @@ class User { /** * The client that created the instance of 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 08eb0091b..b299a58ef 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -102,7 +102,10 @@ const Endpoints = exports.Endpoints = { relationships: `${base}/relationships`, settings: `${base}/settings`, Relationship: uID => `${base}/relationships/${uID}`, - Guild: guildID => `${base}/guilds/${guildID}`, + Guild: guildID => ({ + toString: () => `${base}/guilds/${guildID}`, + settings: `${base}/guilds/${guildID}/settings`, + }), Note: id => `${base}/notes/${id}`, Mentions: (limit, roles, everyone, guildID) => `${base}/mentions?limit=${limit}&roles=${roles}&everyone=${everyone}${guildID ? `&guild_id=${guildID}` : ''}`, @@ -200,7 +203,8 @@ const Endpoints = exports.Endpoints = { const base = `/oauth2/applications/${appID}`; return { toString: () => base, - reset: `${base}/reset`, + resetSecret: `${base}/reset`, + resetToken: `${base}/bot/reset`, }; }, App: appID => `/oauth2/authorize?client_id=${appID}`, @@ -320,6 +324,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', @@ -401,6 +406,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', @@ -432,6 +438,21 @@ exports.MessageTypes = [ 'GUILD_MEMBER_JOIN', ]; +/** + * The type of a message notification setting. Here are the available types: + * * EVERYTHING + * * MENTIONS + * * NOTHING + * * INHERIT (only for GuildChannel) + * @typedef {string} MessageNotificationType + */ +exports.MessageNotificationTypes = [ + 'EVERYTHING', + 'MENTIONS', + 'NOTHING', + 'INHERIT', +]; + exports.DefaultAvatars = { BLURPLE: '6debd47ed13483642cf09e832ed0bc1b', GREY: '322c936a8c8be1b803cd94861bdfa868', @@ -579,6 +600,58 @@ exports.UserSettingsMap = { }, }; +exports.UserGuildSettingsMap = { + message_notifications: function messageNotifications(type) { // eslint-disable-line func-name-matching + /** + * The type of message that should notify you + * @name ClientUserGuildSettings#messageNotifications + * @type {MessageNotificationType} + */ + return exports.MessageNotificationTypes[type]; + }, + /** + * Whether to receive mobile push notifications + * @name ClientUserGuildSettings#mobilePush + * @type {boolean} + */ + mobile_push: 'mobilePush', + /** + * Whether the guild is muted + * @name ClientUserGuildSettings#muted + * @type {boolean} + */ + muted: 'muted', + /** + * Whether to suppress everyone mention + * @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 + * @name ClientUserChannelOverride#messageNotifications + * @type {MessageNotificationType} + */ + return exports.MessageNotificationTypes[type]; + }, + /** + * Whether the channel is muted + * @name ClientUserChannelOverride#muted + * @type {boolean} + */ + muted: 'muted', +}; + exports.Colors = { DEFAULT: 0x000000, AQUA: 0x1ABC9C,