From 801633b9705ff652afdda03fff6922d3c06210c8 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 5 Apr 2017 15:03:33 -0500 Subject: [PATCH] User settings (#1337) * user settings bruh * remove development dump * emit stuff * i am so done * Update ClientUserSettings.js * modularize * Update ClientUserSettings.js * Update Constants.js * Update ClientUserSettings.js * Update RESTMethods.js * Update ClientUserSettings.js * <.< --- src/client/ClientManager.js | 3 +- src/client/rest/RESTMethods.js | 4 + .../packets/WebSocketPacketManager.js | 1 + .../websocket/packets/handlers/Ready.js | 3 +- .../packets/handlers/UserSettingsUpdate.js | 18 +++ src/index.js | 1 + src/structures/ClientUser.js | 16 +- src/structures/ClientUserSettings.js | 77 ++++++++++ src/structures/Guild.js | 34 +++++ src/util/Constants.js | 142 ++++++++++++++++++ src/util/Util.js | 4 +- 11 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 src/client/websocket/packets/handlers/UserSettingsUpdate.js create mode 100644 src/structures/ClientUserSettings.js diff --git a/src/client/ClientManager.js b/src/client/ClientManager.js index 7b1d58a05..4959c7691 100644 --- a/src/client/ClientManager.js +++ b/src/client/ClientManager.js @@ -49,7 +49,8 @@ class ClientManager { * @param {number} time The interval in milliseconds at which heartbeat packets should be sent */ setupKeepAlive(time) { - this.heartbeatInterval = this.client.setInterval(() => this.client.ws.heartbeat(true), time); + this.heartbeatInterval = time; + this.client.setInterval(() => this.client.ws.heartbeat(true), time); } destroy() { diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index 9467e7944..6cba91018 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -876,6 +876,10 @@ class RESTMethods { }) ); } + + patchUserSettings(data) { + return this.rest.makeRequest('patch', Constants.Endpoints.User('@me').settings, true, data); + } } module.exports = RESTMethods; diff --git a/src/client/websocket/packets/WebSocketPacketManager.js b/src/client/websocket/packets/WebSocketPacketManager.js index 53a4f9c3c..34a608f62 100644 --- a/src/client/websocket/packets/WebSocketPacketManager.js +++ b/src/client/websocket/packets/WebSocketPacketManager.js @@ -36,6 +36,7 @@ class WebSocketPacketManager { this.register(Constants.WSEvents.PRESENCE_UPDATE, require('./handlers/PresenceUpdate')); 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.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 6a644ea17..077d17f29 100644 --- a/src/client/websocket/packets/handlers/Ready.js +++ b/src/client/websocket/packets/handlers/Ready.js @@ -9,8 +9,9 @@ class ReadyHandler extends AbstractHandler { client.ws.heartbeat(); + data.user.user_settings = data.user_settings; + const clientUser = new ClientUser(client, data.user); - clientUser.settings = data.user_settings; client.user = clientUser; client.readyAt = new Date(); client.users.set(clientUser.id, clientUser); diff --git a/src/client/websocket/packets/handlers/UserSettingsUpdate.js b/src/client/websocket/packets/handlers/UserSettingsUpdate.js new file mode 100644 index 000000000..24085cf4d --- /dev/null +++ b/src/client/websocket/packets/handlers/UserSettingsUpdate.js @@ -0,0 +1,18 @@ +const AbstractHandler = require('./AbstractHandler'); +const Constants = require('../../../../util/Constants'); + +class UserSettingsUpdateHandler extends AbstractHandler { + handle(packet) { + const client = this.packetManager.client; + client.user.settings.patch(packet.d); + client.emit(Constants.Events.USER_SETTINGS_UPDATE, client.user.settings); + } +} + +/** + * Emitted when the client user's settings update + * @event Client#clientUserSettingsUpdate + * @param {ClientUserSettings} clientUserSettings The new client user settings + */ + +module.exports = UserSettingsUpdateHandler; diff --git a/src/index.js b/src/index.js index 3abc36a13..b5135973e 100644 --- a/src/index.js +++ b/src/index.js @@ -27,6 +27,7 @@ module.exports = { // Structures Channel: require('./structures/Channel'), ClientUser: require('./structures/ClientUser'), + ClientUserSettings: require('./structures/ClientUserSettings'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), Game: require('./structures/Presence').Game, diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 15bdb6368..bd54f2390 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -1,6 +1,6 @@ const User = require('./User'); const Collection = require('../util/Collection'); - +const ClientUserSettings = require('./ClientUserSettings'); /** * Represents the logged in client's Discord user * @extends {User} @@ -44,13 +44,6 @@ class ClientUser extends User { */ this.notes = new Collection(); - /** - * Discord client settings, such as guild positions - * This is only filled when using a user account. - * @type {Object} - */ - this.settings = {}; - /** * If the user has discord premium (nitro) * This is only filled when using a user account. @@ -71,6 +64,13 @@ class ClientUser extends User { * @type {?boolean} */ this.mobile = typeof data.mobile === 'boolean' ? data.mobile : null; + + /** + * Various settings for this user + * @type {?ClientUserSettings} + * This is only filled when using a user account + */ + if (data.user_settings) this.settings = new ClientUserSettings(this, data.user_settings); } edit(data) { diff --git a/src/structures/ClientUserSettings.js b/src/structures/ClientUserSettings.js new file mode 100644 index 000000000..07b23fbca --- /dev/null +++ b/src/structures/ClientUserSettings.js @@ -0,0 +1,77 @@ +const Constants = require('../util/Constants'); +const Util = require('../util/Util'); + +/** + * 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 + */ + patch(data) { + for (const key of Object.keys(Constants.UserSettingsMap)) { + const value = Constants.UserSettingsMap[key]; + 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 Value to patch + * @returns {Promise} + */ + update(name, value) { + return this.user.client.rest.methods.patchUserSettings({ [name]: value }); + } + + /** + * @param {Guild} guild 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); + } + + /** + * Add a guild to the list of restricted guilds + * @param {Guild} guild Guild to add + * @returns {Promise} + */ + addRestrictedGuild(guild) { + const temp = Object.assign([], this.restrictedGuilds); + if (temp.includes(guild.id)) return Promise.reject(new Error('Guild is already restricted')); + temp.push(guild.id); + return this.update('restricted_guilds', temp).then(() => guild); + } + + /** + * Remove a guild from the list of restricted guilds + * @param {Guild} guild 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 is not restricted')); + temp.splice(index, 1); + return this.update('restricted_guilds', temp).then(() => guild); + } +} + +module.exports = ClientUserSettings; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index eb3cd6e44..c79b9ec41 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -301,6 +301,17 @@ class Guild { } /** + * Get the position of this guild + * This is only available when using a user account. + * @type {?number} + */ + 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); + } + + /* * The `@everyone` Role of the guild. * @type {Role} * @readonly @@ -786,6 +797,29 @@ class Guild { return this.client.rest.methods.ackGuild(this); } + /** + * @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('Setting guild position is only available for user accounts')); + } + return this.client.user.settings.setGuildPosition(this, position, relative); + } + + /** + * Allow direct messages from guild members + * @param {boolean} allow Whether to allow direct messages + * @returns {Promise} + */ + allowDMs(allow) { + const settings = this.client.user.settings; + if (allow) return settings.removeRestrictedGuild(this); + else return settings.addRestrictedGuild(this); + } + /** * Whether this Guild equals another Guild. It compares all properties, so for most operations * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often diff --git a/src/util/Constants.js b/src/util/Constants.js index 61de117fb..88519a95e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -310,6 +310,7 @@ exports.Events = { MESSAGE_REACTION_REMOVE_ALL: 'messageReactionRemoveAll', USER_UPDATE: 'userUpdate', USER_NOTE_UPDATE: 'userNoteUpdate', + USER_SETTINGS_UPDATE: 'clientUserSettingsUpdate', PRESENCE_UPDATE: 'presenceUpdate', VOICE_STATE_UPDATE: 'voiceStateUpdate', TYPING_START: 'typingStart', @@ -350,6 +351,7 @@ exports.Events = { * - MESSAGE_REACTION_REMOVE_ALL * - USER_UPDATE * - USER_NOTE_UPDATE + * - USER_SETTINGS_UPDATE * - PRESENCE_UPDATE * - VOICE_STATE_UPDATE * - TYPING_START @@ -388,6 +390,7 @@ exports.WSEvents = { MESSAGE_REACTION_REMOVE_ALL: 'MESSAGE_REACTION_REMOVE_ALL', USER_UPDATE: 'USER_UPDATE', USER_NOTE_UPDATE: 'USER_NOTE_UPDATE', + USER_SETTINGS_UPDATE: 'USER_SETTINGS_UPDATE', PRESENCE_UPDATE: 'PRESENCE_UPDATE', VOICE_STATE_UPDATE: 'VOICE_STATE_UPDATE', TYPING_START: 'TYPING_START', @@ -416,6 +419,145 @@ exports.DefaultAvatars = { RED: '1cbd08c76f8af6dddce02c5138971129', }; +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 uploaded 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, + }; + }, +}; + exports.Colors = { DEFAULT: 0x000000, AQUA: 0x1ABC9C, diff --git a/src/util/Util.js b/src/util/Util.js index c28abd38b..cbc8cb152 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -197,7 +197,7 @@ class Util { * @param {*} element Element to move * @param {number} newIndex Index or offset to move the element to * @param {boolean} [offset=false] Move the element by an offset amount rather than to a set index - * @returns {Array<*>} + * @returns {number} * @private */ static moveElementInArray(array, element, newIndex, offset = false) { @@ -207,7 +207,7 @@ class Util { const removedElement = array.splice(index, 1)[0]; array.splice(newIndex, 0, removedElement); } - return array; + return array.indexOf(element); } }