From cf7dcba1a59805b7b9398a5ac7d6688641bbe3bb Mon Sep 17 00:00:00 2001 From: Will Nelson Date: Thu, 1 Mar 2018 21:00:21 -0800 Subject: [PATCH] Add toJSON methods (#1859) * tojson things * fix client * ignore private properties * remove extra property descriptors * handle primitive flattening * remove unused import * add toJSON to collections * reduce stateful props * state * allow custom prop names when flattening * fix client * fix build * fix flatten docs * remove guild.available, cleanup permissions, remove arbitrary id reduction * fix util import * add valueOf as needed, update member props * fix incorrect merge * update permissionoverwrites and permissions remove serialization of permissions in PermissionOverwrites#toJSON. change Permissions#toJSON to serialize permissions, by default excluding admin checks. * change Permissions#toJSON to return the primitive * Permissions#toJSON explicitly return bitfield --- src/client/BaseClient.js | 4 +++ src/client/Client.js | 9 +++++++ src/structures/Base.js | 10 ++++++++ src/structures/Channel.js | 4 +++ src/structures/ClientApplication.js | 4 +++ src/structures/ClientUser.js | 10 ++++++++ src/structures/Emoji.js | 9 +++++++ src/structures/Guild.js | 13 ++++++++++ src/structures/GuildAuditLogs.js | 9 +++++++ src/structures/GuildMember.js | 11 ++++++++ src/structures/Invite.js | 15 +++++++++++ src/structures/Message.js | 12 +++++++++ src/structures/MessageAttachment.js | 6 +++++ src/structures/MessageEmbed.js | 4 +++ src/structures/MessageMentions.js | 8 ++++++ src/structures/MessageReaction.js | 6 +++++ src/structures/PermissionOverwrites.js | 5 ++++ src/structures/Presence.js | 5 ++++ src/structures/ReactionEmoji.js | 9 +++++++ src/structures/Role.js | 4 +++ src/structures/User.js | 13 ++++++++++ src/structures/UserConnection.js | 6 +++++ src/structures/UserProfile.js | 4 +++ src/structures/VoiceRegion.js | 6 +++++ src/structures/interfaces/Collector.js | 5 ++++ src/util/Collection.js | 6 +++++ src/util/Permissions.js | 4 +++ src/util/Util.js | 35 ++++++++++++++++++++++++++ 28 files changed, 236 insertions(+) diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index f2c91ccdf..0d0e32caf 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -105,6 +105,10 @@ class BaseClient extends EventEmitter { clearInterval(interval); this._intervals.delete(interval); } + + toJSON(...props) { + return Util.flatten(this, { domain: false }, ...props); + } } module.exports = BaseClient; diff --git a/src/client/Client.js b/src/client/Client.js index 475d51d62..beacfcc6e 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -411,6 +411,15 @@ class Client extends BaseClient { ); } + toJSON() { + 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 diff --git a/src/structures/Base.js b/src/structures/Base.js index 64e4a8560..37633757b 100644 --- a/src/structures/Base.js +++ b/src/structures/Base.js @@ -1,3 +1,5 @@ +const Util = require('../util/Util'); + /** * Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models). */ @@ -23,6 +25,14 @@ class Base { this._patch(data); return clone; } + + toJSON(...props) { + return Util.flatten(this, ...props); + } + + valueOf() { + return this.id; + } } module.exports = Base; diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 49248275a..52d87f092 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -114,6 +114,10 @@ class Channel extends Base { } return channel; } + + toJSON(...props) { + return super.toJSON({ createdTimestamp: true }, ...props); + } } module.exports = Channel; diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 8c968b0da..f76e98b0a 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -205,6 +205,10 @@ class ClientApplication extends Base { toString() { return this.name; } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } } module.exports = ClientApplication; diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 900d8258e..cce9957ad 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -327,6 +327,16 @@ 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/Emoji.js b/src/structures/Emoji.js index f5682046f..419db88d1 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -61,6 +61,15 @@ class Emoji extends Base { toString() { return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name; } + + toJSON() { + return super.toJSON({ + guild: 'guildID', + createdTimestamp: true, + url: true, + identifier: true, + }); + } } module.exports = Emoji; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9feb8b802..7ac1f88be 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -951,6 +951,19 @@ class Guild extends Base { return this.name; } + toJSON() { + const json = super.toJSON({ + available: false, + createdTimestamp: true, + nameAcronym: true, + presences: false, + voiceStates: false, + }); + json.iconURL = this.iconURL(); + json.splashURL = this.splashURL(); + return json; + } + /** * Creates a collection of this guild's roles, sorted by their position and IDs. * @returns {Collection} diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js index 6df77707e..86f3cf10a 100644 --- a/src/structures/GuildAuditLogs.js +++ b/src/structures/GuildAuditLogs.js @@ -1,6 +1,7 @@ const Collection = require('../util/Collection'); const Snowflake = require('../util/Snowflake'); const Webhook = require('./Webhook'); +const Util = require('../util/Util'); /** * The target type of an entry, e.g. `GUILD`. Here are the available types: @@ -220,6 +221,10 @@ class GuildAuditLogs { return 'ALL'; } + + toJSON() { + return Util.flatten(this); + } } /** @@ -371,6 +376,10 @@ class GuildAuditLogsEntry { get createdAt() { return new Date(this.createdTimestamp); } + + toJSON() { + return Util.flatten(this, { createdTimestamp: true }); + } } GuildAuditLogs.Actions = Actions; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index be70e315e..03f767e90 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -419,6 +419,17 @@ class GuildMember extends Base { return `<@${this.nickname ? '!' : ''}${this.user.id}>`; } + toJSON() { + return super.toJSON({ + guild: 'guildID', + user: 'userID', + displayName: true, + speaking: false, + lastMessage: false, + lastMessageID: false, + }); + } + // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} diff --git a/src/structures/Invite.js b/src/structures/Invite.js index 1f02d11b7..b10c1de24 100644 --- a/src/structures/Invite.js +++ b/src/structures/Invite.js @@ -149,6 +149,21 @@ class Invite extends Base { toString() { return this.url; } + + toJSON() { + return super.toJSON({ + url: true, + expiresTimestamp: true, + presenceCount: false, + memberCount: false, + textChannelCount: false, + voiceChannelCount: false, + uses: false, + channel: 'channelID', + inviter: 'inviterID', + guild: 'guildID', + }); + } } module.exports = Invite; diff --git a/src/structures/Message.js b/src/structures/Message.js index 9736819d4..0b399935e 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -556,6 +556,18 @@ class Message extends Base { toString() { return this.content; } + + toJSON() { + return super.toJSON({ + channel: 'channelID', + author: 'authorID', + application: 'applicationID', + guild: 'guildID', + cleanContent: true, + member: false, + reactions: false, + }); + } } module.exports = Message; diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 7a97a3790..fcb82d7a4 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -1,3 +1,5 @@ +const Util = require('../util/Util'); + /** * Represents an attachment in a message. * @param {BufferResolvable|Stream} file The file @@ -108,6 +110,10 @@ class MessageAttachment { */ this.width = data.width; } + + toJSON() { + return Util.flatten(this); + } } module.exports = MessageAttachment; diff --git a/src/structures/MessageEmbed.js b/src/structures/MessageEmbed.js index 297b19fff..82d930eda 100644 --- a/src/structures/MessageEmbed.js +++ b/src/structures/MessageEmbed.js @@ -302,6 +302,10 @@ class MessageEmbed { return this; } + toJSON() { + return Util.flatten(this, { hexColor: true }); + } + /** * Transforms the embed object to be processed. * @returns {Object} The raw data of this embed diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js index 102e63aaa..c3bb585cb 100644 --- a/src/structures/MessageMentions.js +++ b/src/structures/MessageMentions.js @@ -1,4 +1,5 @@ const Collection = require('../util/Collection'); +const Util = require('../util/Util'); const GuildMember = require('./GuildMember'); /** @@ -139,6 +140,13 @@ class MessageMentions { return false; } + + toJSON() { + return Util.flatten(this, { + members: true, + channels: true, + }); + } } /** diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index 660967f0b..cb36a6c30 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -1,4 +1,5 @@ const GuildEmoji = require('./GuildEmoji'); +const Util = require('../util/Util'); const ReactionEmoji = require('./ReactionEmoji'); const ReactionUserStore = require('../stores/ReactionUserStore'); @@ -55,6 +56,11 @@ class MessageReaction { return this._emoji; } + + toJSON() { + return Util.flatten(this, { emoji: 'emojiID', message: 'messageID' }); + } + _add(user) { if (!this.users.has(user.id)) { this.users.set(user.id, user); diff --git a/src/structures/PermissionOverwrites.js b/src/structures/PermissionOverwrites.js index 27a87764b..5533a95a8 100644 --- a/src/structures/PermissionOverwrites.js +++ b/src/structures/PermissionOverwrites.js @@ -1,4 +1,5 @@ const Permissions = require('../util/Permissions'); +const Util = require('../util/Util'); /** * Represents a permission overwrite for a role or member in a guild channel. @@ -59,6 +60,10 @@ class PermissionOverwrites { .delete({ reason }) .then(() => this); } + + toJSON() { + return Util.flatten(this); + } } module.exports = PermissionOverwrites; diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 42c2abaa2..dc8ff1ef2 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -1,3 +1,4 @@ +const Util = require('../util/Util'); const { ActivityTypes, ActivityFlags } = require('../util/Constants'); /** @@ -56,6 +57,10 @@ class Presence { this.activity ? this.activity.equals(presence.activity) : !presence.activity ); } + + toJSON() { + return Util.flatten(this); + } } /** diff --git a/src/structures/ReactionEmoji.js b/src/structures/ReactionEmoji.js index 9bb23c120..c6ea8d68f 100644 --- a/src/structures/ReactionEmoji.js +++ b/src/structures/ReactionEmoji.js @@ -1,3 +1,4 @@ +const Util = require('../util/Util'); const Emoji = require('./Emoji'); /** @@ -15,6 +16,14 @@ class ReactionEmoji extends Emoji { */ this.reaction = reaction; } + + toJSON() { + return Util.flatten(this, { identifier: true }); + } + + valueOf() { + return this.id; + } } module.exports = ReactionEmoji; diff --git a/src/structures/Role.js b/src/structures/Role.js index adddc5831..58124b3b9 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -353,6 +353,10 @@ class Role extends Base { return `<@&${this.id}>`; } + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } + /** * Compares the positions of two roles. * @param {Role} role1 First role to compare diff --git a/src/structures/User.js b/src/structures/User.js index 001a999be..95b00c208 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -258,6 +258,19 @@ class User extends Base { return `<@${this.id}>`; } + toJSON(...props) { + const json = super.toJSON({ + createdTimestamp: true, + defaultAvatarURL: true, + tag: true, + lastMessage: false, + lastMessageID: false, + }, ...props); + json.avatarURL = this.avatarURL(); + json.displayAvatarURL = this.displayAvatarURL(); + return json; + } + // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ send() {} diff --git a/src/structures/UserConnection.js b/src/structures/UserConnection.js index 3bcdb0894..698e5b039 100644 --- a/src/structures/UserConnection.js +++ b/src/structures/UserConnection.js @@ -1,3 +1,5 @@ +const Util = require('../util/Util'); + /** * Represents a user connection (or "platform identity"). */ @@ -43,6 +45,10 @@ class UserConnection { */ this.integrations = data.integrations; } + + toJSON() { + return Util.flatten(this); + } } module.exports = UserConnection; diff --git a/src/structures/UserProfile.js b/src/structures/UserProfile.js index 07cfdf96e..704cb0748 100644 --- a/src/structures/UserProfile.js +++ b/src/structures/UserProfile.js @@ -74,6 +74,10 @@ class UserProfile extends Base { } return flags; } + + toJSON() { + return super.toJSON({ flags: true }); + } } module.exports = UserProfile; diff --git a/src/structures/VoiceRegion.js b/src/structures/VoiceRegion.js index dc6b46171..68c9e93cf 100644 --- a/src/structures/VoiceRegion.js +++ b/src/structures/VoiceRegion.js @@ -1,3 +1,5 @@ +const Util = require('../util/Util'); + /** * Represents a Discord voice region for guilds. */ @@ -45,6 +47,10 @@ class VoiceRegion { */ this.sampleHostname = data.sample_hostname; } + + toJSON() { + return Util.flatten(this); + } } module.exports = VoiceRegion; diff --git a/src/structures/interfaces/Collector.js b/src/structures/interfaces/Collector.js index 0578dadfd..1f7394e47 100644 --- a/src/structures/interfaces/Collector.js +++ b/src/structures/interfaces/Collector.js @@ -1,4 +1,5 @@ const Collection = require('../../util/Collection'); +const Util = require('../../util/Util'); const EventEmitter = require('events'); /** @@ -172,6 +173,10 @@ class Collector extends EventEmitter { if (reason) this.stop(reason); } + toJSON() { + return Util.flatten(this); + } + /* eslint-disable no-empty-function, valid-jsdoc */ /** * Handles incoming events from the `handleCollect` function. Returns null if the event should not diff --git a/src/util/Collection.js b/src/util/Collection.js index b1205be16..4195fc8a9 100644 --- a/src/util/Collection.js +++ b/src/util/Collection.js @@ -1,3 +1,5 @@ +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. @@ -425,6 +427,10 @@ class Collection extends Map { 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]))); } + + toJSON() { + return this.map(e => typeof e.toJSON === 'function' ? e.toJSON() : Util.flatten(e)); + } } module.exports = Collection; diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 17da87107..0a9c4a169 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -102,6 +102,10 @@ class Permissions { return Object.keys(this.constructor.FLAGS).filter(perm => this.has(perm, checkAdmin)); } + toJSON() { + return this.bitfield; + } + valueOf() { return this.bitfield; } diff --git a/src/util/Util.js b/src/util/Util.js index 6cb22a705..d27ed9a60 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -12,6 +12,41 @@ class Util { throw new Error(`The ${this.constructor.name} class may not be instantiated.`); } + /** + * Flatten an object. Any properties that are collections will get converted to an array of keys. + * @param {Object} obj The object to flatten. + * @param {...Object} [props] Specific properties to include/exclude. + * @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); + + const out = {}; + + for (let [prop, newProp] of Object.entries(props)) { + if (!newProp) continue; + newProp = newProp === true ? prop : newProp; + + const element = obj[prop]; + 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 (element instanceof require('./Collection')) out[newProp] = Array.from(element.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 + else if (valueOf && !isObject(valueOf)) out[newProp] = valueOf; + // If it's a primitive + else if (!elemIsObj) out[newProp] = element; + } + + return out; + } + /** * Splits a string into multiple chunks at a designated character that do not exceed a specific length. * @param {string} text Content to split