From dd085ceaee1903f5fbd7424f37b802ddfcc0b74e Mon Sep 17 00:00:00 2001 From: bdistin Date: Fri, 8 Sep 2017 16:06:10 -0500 Subject: [PATCH] Datastore cleanup (#1892) * Start Store cleanup * wip store cleanup * fix iterables initiating datastores * handle the possibility of a datastore with no holds and no its own 'create' method * more cleanup (instances that need more than just client/data) * missed RoleStore extras * not sure how eslint didn't catch that tab... * avoid re-getting the channel we already have... * cleanup resolvers and refactor them into DataStores * ^ * and remove * fix some bugs * fix lint * fix documentation maybe? * formatting * fix presences * really fix presences this time * bad fix was bad... let;s see how bad this one is.. * forgot to save a file * make presence resolving take userresolveables too * fix tabs in jsdocs * fix bad fix from last night that caused issues, with better fix... * oops --- src/client/BaseClient.js | 8 - src/client/Client.js | 3 +- src/client/ClientDataResolver.js | 282 ------------------ src/client/voice/receiver/VoiceReceiver.js | 4 +- src/index.js | 1 + src/stores/ChannelStore.js | 25 +- src/stores/DataStore.js | 36 ++- src/stores/EmojiStore.js | 60 +++- src/stores/GuildChannelStore.js | 26 +- src/stores/GuildMemberStore.js | 44 ++- src/stores/GuildStore.js | 34 ++- src/stores/MessageStore.js | 41 ++- src/stores/PresenceStore.js | 47 ++- src/stores/ReactionStore.js | 39 ++- src/stores/RoleStore.js | 36 ++- src/stores/UserStore.js | 40 ++- src/structures/ClientApplication.js | 3 +- src/structures/ClientUser.js | 7 +- src/structures/Emoji.js | 8 +- src/structures/GroupDMChannel.js | 7 +- src/structures/Guild.js | 35 +-- src/structures/GuildChannel.js | 14 +- src/structures/GuildMember.js | 16 +- src/structures/Message.js | 8 +- src/structures/MessageReaction.js | 10 +- src/structures/Presence.js | 2 + src/structures/Role.js | 6 +- src/structures/TextChannel.js | 3 +- src/structures/User.js | 6 +- src/structures/Webhook.js | 5 +- src/structures/interfaces/TextBasedChannel.js | 7 +- src/structures/shared/Search.js | 6 +- src/structures/shared/SendMessage.js | 2 +- src/util/DataResolver.js | 124 ++++++++ 34 files changed, 560 insertions(+), 435 deletions(-) delete mode 100644 src/client/ClientDataResolver.js create mode 100644 src/util/DataResolver.js diff --git a/src/client/BaseClient.js b/src/client/BaseClient.js index 76b57b5cb..349455f13 100644 --- a/src/client/BaseClient.js +++ b/src/client/BaseClient.js @@ -1,6 +1,5 @@ const EventEmitter = require('events'); const RESTManager = require('../rest/RESTManager'); -const ClientDataResolver = require('./ClientDataResolver'); const Util = require('../util/Util'); const Constants = require('../util/Constants'); @@ -25,13 +24,6 @@ class BaseClient extends EventEmitter { */ this.rest = new RESTManager(this, options._tokenType); - /** - * The data resolver of the client - * @type {ClientDataResolver} - * @private - */ - this.resolver = new ClientDataResolver(this); - /** * Timeouts set by {@link WebhookClient#setTimeout} that are still active * @type {Set} diff --git a/src/client/Client.js b/src/client/Client.js index 147e4afa2..7712cd0d9 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -17,6 +17,7 @@ const ChannelStore = require('../stores/ChannelStore'); const GuildStore = require('../stores/GuildStore'); const ClientPresenceStore = require('../stores/ClientPresenceStore'); const Constants = require('../util/Constants'); +const DataResolver = require('../util/DataResolver'); const { Error, TypeError, RangeError } = require('../errors'); /** @@ -292,7 +293,7 @@ class Client extends BaseClient { * @returns {Promise} */ fetchInvite(invite) { - const code = this.resolver.resolveInviteCode(invite); + const code = DataResolver.resolveInviteCode(invite); return this.api.invites(code).get({ query: { with_counts: true } }) .then(data => new Invite(this, data)); } diff --git a/src/client/ClientDataResolver.js b/src/client/ClientDataResolver.js deleted file mode 100644 index 7066c05a1..000000000 --- a/src/client/ClientDataResolver.js +++ /dev/null @@ -1,282 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const snekfetch = require('snekfetch'); - -const Util = require('../util/Util'); -const User = require('../structures/User'); -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'); - -/** - * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g. - * extracting a User from a Message object. - * @private - */ -class ClientDataResolver { - /** - * @param {Client} client The client the resolver is for - */ - constructor(client) { - this.client = client; - } - - /** - * Data that resolves to give a User object. This can be: - * * A User object - * * A Snowflake - * * A Message object (resolves to the message author) - * * A GuildMember object - * @typedef {User|Snowflake|Message|GuildMember} UserResolvable - */ - - /** - * Resolves a UserResolvable to a User object. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?User} - */ - resolveUser(user) { - if (user instanceof User) return user; - if (typeof user === 'string') return this.client.users.get(user) || null; - if (user instanceof GuildMember) return user.user; - if (user instanceof Message) return user.author; - return null; - } - - /** - * Resolves a UserResolvable to a user ID string. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?Snowflake} - */ - resolveUserID(user) { - if (user instanceof User || user instanceof GuildMember) return user.id; - if (typeof user === 'string') return user || null; - if (user instanceof Message) return user.author.id; - return null; - } - - /** - * Data that resolves to give a Guild object. This can be: - * * A Guild object - * * A Snowflake - * @typedef {Guild|Snowflake} GuildResolvable - */ - - /** - * Resolves a GuildResolvable to a Guild object. - * @param {GuildResolvable} guild The GuildResolvable to identify - * @returns {?Guild} - */ - resolveGuild(guild) { - if (guild instanceof Guild) return guild; - if (typeof guild === 'string') return this.client.guilds.get(guild) || null; - return null; - } - - /** - * Data that resolves to give a GuildMember object. This can be: - * * A GuildMember object - * * A User object - * @typedef {GuildMember|User} GuildMemberResolvable - */ - - /** - * Resolves a GuildMemberResolvable to a GuildMember object. - * @param {GuildResolvable} guild The guild that the member is part of - * @param {UserResolvable} user The user that is part of the guild - * @returns {?GuildMember} - */ - resolveGuildMember(guild, user) { - if (user instanceof GuildMember) return user; - guild = this.resolveGuild(guild); - user = this.resolveUser(user); - if (!guild || !user) return null; - 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 - * * A Snowflake - * @typedef {Channel|Snowflake} ChannelResolvable - */ - - /** - * Resolves a ChannelResolvable to a Channel object. - * @param {ChannelResolvable} channel The channel resolvable to resolve - * @returns {?Channel} - */ - resolveChannel(channel) { - if (channel instanceof Channel) return channel; - if (typeof channel === 'string') return this.client.channels.get(channel) || null; - return null; - } - - /** - * Resolves a ChannelResolvable to a channel ID. - * @param {ChannelResolvable} channel The channel resolvable to resolve - * @returns {?Snowflake} - */ - resolveChannelID(channel) { - if (channel instanceof Channel) return channel.id; - if (typeof channel === 'string') return channel; - return null; - } - - /** - * Data that can be resolved to give an invite code. This can be: - * * An invite code - * * An invite URL - * @typedef {string} InviteResolvable - */ - - /** - * Resolves InviteResolvable to an invite code. - * @param {InviteResolvable} data The invite resolvable to resolve - * @returns {string} - */ - resolveInviteCode(data) { - const inviteRegex = /discord(?:app\.com\/invite|\.gg)\/([\w-]{2,255})/i; - const match = inviteRegex.exec(data); - if (match && match[1]) return match[1]; - return data; - } - - /** - * Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image. - * @param {BufferResolvable|Base64Resolvable} image The image to be resolved - * @returns {Promise} - */ - async resolveImage(image) { - if (!image) return null; - if (typeof image === 'string' && image.startsWith('data:')) { - return image; - } - const file = await this.resolveFile(image); - return this.resolveBase64(file); - } - - /** - * Data that resolves to give a Base64 string, typically for image uploading. This can be: - * * A Buffer - * * A base64 string - * @typedef {Buffer|string} Base64Resolvable - */ - - /** - * Resolves a Base64Resolvable to a Base 64 image. - * @param {Base64Resolvable} data The base 64 resolvable you want to resolve - * @returns {?string} - */ - resolveBase64(data) { - if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`; - return data; - } - - /** - * Data that can be resolved to give a Buffer. This can be: - * * A Buffer - * * The path to a local file - * * A URL - * @typedef {string|Buffer} BufferResolvable - */ - - /** - * @external Stream - * @see {@link https://nodejs.org/api/stream.html} - */ - - /** - * Resolves a BufferResolvable to a Buffer. - * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve - * @returns {Promise} - */ - resolveFile(resource) { - if (resource instanceof Buffer) return Promise.resolve(resource); - if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(Util.convertToBuffer(resource)); - - if (typeof resource === 'string') { - return new Promise((resolve, reject) => { - if (/^https?:\/\//.test(resource)) { - snekfetch.get(resource) - .end((err, res) => { - if (err) return reject(err); - 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('FILE_NOT_FOUND', file)); - fs.readFile(file, (err2, data) => { - if (err2) reject(err2); else resolve(data); - }); - return null; - }); - } - }); - } 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')); - } - - /** - * Data that can be resolved to give an emoji identifier. This can be: - * * The unicode representation of an emoji - * * A custom emoji ID - * * An Emoji object - * * A ReactionEmoji object - * @typedef {string|Snowflake|Emoji|ReactionEmoji} EmojiIdentifierResolvable - */ - - /** - * Resolves an EmojiResolvable to an emoji identifier. - * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve - * @returns {?string} - */ - resolveEmojiIdentifier(emoji) { - if (emoji instanceof Emoji || emoji instanceof ReactionEmoji) return emoji.identifier; - if (typeof emoji === 'string') { - if (this.client.emojis.has(emoji)) return this.client.emojis.get(emoji).identifier; - else if (!emoji.includes('%')) return encodeURIComponent(emoji); - else return emoji; - } - return null; - } -} - -module.exports = ClientDataResolver; diff --git a/src/client/voice/receiver/VoiceReceiver.js b/src/client/voice/receiver/VoiceReceiver.js index d8b3010da..4b697ce4e 100644 --- a/src/client/voice/receiver/VoiceReceiver.js +++ b/src/client/voice/receiver/VoiceReceiver.js @@ -123,7 +123,7 @@ class VoiceReceiver extends EventEmitter { * @returns {ReadableStream} */ createOpusStream(user) { - user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); + user = this.voiceConnection.voiceManager.client.users.resolve(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(); @@ -138,7 +138,7 @@ class VoiceReceiver extends EventEmitter { * @returns {ReadableStream} */ createPCMStream(user) { - user = this.voiceConnection.voiceManager.client.resolver.resolveUser(user); + user = this.voiceConnection.voiceManager.client.users.resolve(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(); diff --git a/src/index.js b/src/index.js index 4848968b0..e181679d6 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ module.exports = { // Utilities Collection: require('./util/Collection'), Constants: require('./util/Constants'), + DataResolver: require('./util/DataResolver'), DiscordAPIError: require('./rest/DiscordAPIError'), EvaluatedPermissions: require('./util/Permissions'), Permissions: require('./util/Permissions'), diff --git a/src/stores/ChannelStore.js b/src/stores/ChannelStore.js index 2adb39a20..943cad399 100644 --- a/src/stores/ChannelStore.js +++ b/src/stores/ChannelStore.js @@ -16,7 +16,7 @@ class ChannelStore extends DataStore { options = iterableOrOptions; iterableOrOptions = undefined; } - super(client, iterableOrOptions); + super(client, iterableOrOptions, Channel); if (options.lru) { const lru = this[kLru] = []; @@ -72,6 +72,29 @@ class ChannelStore extends DataStore { if (channel.guild) channel.guild.channels.remove(id); super.remove(id); } + + /** + * Data that can be resolved to give a Channel object. This can be: + * * A Channel object + * * A Snowflake + * @typedef {Channel|Snowflake} ChannelResolvable + */ + + /** + * Resolves a ChannelResolvable to a Channel object. + * @method resolve + * @memberof ChannelStore + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?Channel} + */ + + /** + * Resolves a ChannelResolvable to a channel ID string. + * @method resolveID + * @memberof ChannelStore + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?string} + */ } module.exports = ChannelStore; diff --git a/src/stores/DataStore.js b/src/stores/DataStore.js index b66d08880..1aede94f6 100644 --- a/src/stores/DataStore.js +++ b/src/stores/DataStore.js @@ -5,15 +5,45 @@ const Collection = require('../util/Collection'); * @extends {Collection} */ class DataStore extends Collection { - constructor(client, iterable) { + constructor(client, iterable, holds) { super(); Object.defineProperty(this, 'client', { value: client }); + Object.defineProperty(this, 'holds', { value: holds }); if (iterable) for (const item of iterable) this.create(item); } - // Stubs - create() { return undefined; } + create(data, cache = true, { id, extras = [] } = {}) { + const existing = this.get(id || data.id); + if (existing) return existing; + + const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; + if (cache) this.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 + */ + resolve(idOrInstance) { + if (idOrInstance instanceof this.holds) return idOrInstance; + if (typeof idOrInstance === 'string') return this.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 + * @returns {?string} + */ + resolveID(idOrInstance) { + if (idOrInstance instanceof this.holds) return idOrInstance.id; + if (typeof channel === 'string') return idOrInstance; + return null; + } } module.exports = DataStore; diff --git a/src/stores/EmojiStore.js b/src/stores/EmojiStore.js index e548760a6..dbe98b7b5 100644 --- a/src/stores/EmojiStore.js +++ b/src/stores/EmojiStore.js @@ -1,5 +1,7 @@ const DataStore = require('./DataStore'); const Emoji = require('../structures/Emoji'); +const ReactionEmoji = require('../structures/ReactionEmoji'); + /** * Stores emojis. * @private @@ -7,20 +9,62 @@ const Emoji = require('../structures/Emoji'); */ class EmojiStore extends DataStore { constructor(guild, iterable) { - super(guild.client, iterable); + super(guild.client, iterable, Emoji); this.guild = guild; } - create(data) { - const guild = this.guild; + create(data, cache) { + super.create(data, cache, { extras: [this.guild] }); + } - const existing = guild.emojis.get(data.id); - if (existing) return existing; + /** + * Data that can be resolved into an Emoji object. This can be: + * * A custom emoji ID + * * An Emoji object + * * A ReactionEmoji object + * @typedef {Snowflake|Emoji|ReactionEmoji} EmojiResolvable + */ - const emoji = new Emoji(guild, data); - guild.emojis.set(emoji.id, emoji); + /** + * Resolves a EmojiResolvable to a Emoji object. + * @param {EmojiResolvable} emoji The Emoji resolvable to identify + * @returns {?Emoji} + */ + resolve(emoji) { + if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id); + return super.resolve(emoji); + } - return emoji; + /** + * Resolves a EmojiResolvable to a Emoji ID string. + * @param {EmojiResolvable} emoji The Emoji resolvable to identify + * @returns {?string} + */ + resolveID(emoji) { + if (emoji instanceof ReactionEmoji) return emoji.id; + return super.resolveID(emoji); + } + + /** + * Data that can be resolved to give an emoji identifier. This can be: + * * The unicode representation of an emoji + * * An EmojiResolveable + * @typedef {string|EmojiResolvable} EmojiIdentifierResolvable + */ + + /** + * Resolves an EmojiResolvable to an emoji identifier. + * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve + * @returns {?string} + */ + resolveIdentifier(emoji) { + const emojiResolveable = this.resolve(emoji); + if (emojiResolveable) return emojiResolveable.identifier; + if (typeof emoji === 'string') { + if (!emoji.includes('%')) return encodeURIComponent(emoji); + else return emoji; + } + return null; } } diff --git a/src/stores/GuildChannelStore.js b/src/stores/GuildChannelStore.js index f6e4709a4..7af33c5c9 100644 --- a/src/stores/GuildChannelStore.js +++ b/src/stores/GuildChannelStore.js @@ -1,5 +1,6 @@ const DataStore = require('./DataStore'); const Channel = require('../structures/Channel'); +const GuildChannel = require('../structures/GuildChannel'); /** * Stores guild channels. @@ -8,7 +9,7 @@ const Channel = require('../structures/Channel'); */ class GuildChannelStore extends DataStore { constructor(guild, iterable) { - super(guild.client, iterable); + super(guild.client, iterable, GuildChannel); this.guild = guild; } @@ -18,6 +19,29 @@ class GuildChannelStore extends DataStore { return Channel.create(this.client, data, this.guild); } + + /** + * Data that can be resolved to give a Channel object. This can be: + * * A GuildChannel object + * * A Snowflake + * @typedef {Channel|Snowflake} GuildChannelResolvable + */ + + /** + * Resolves a GuildChannelResolvable to a Channel object. + * @method resolve + * @memberof GuildChannelStore + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?Channel} + */ + + /** + * Resolves a GuildChannelResolvable to a channel ID string. + * @method resolveID + * @memberof GuildChannelStore + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?string} + */ } module.exports = GuildChannelStore; diff --git a/src/stores/GuildMemberStore.js b/src/stores/GuildMemberStore.js index d81029cf8..1cbc53b58 100644 --- a/src/stores/GuildMemberStore.js +++ b/src/stores/GuildMemberStore.js @@ -10,18 +10,44 @@ const { Error } = require('../errors'); */ class GuildMemberStore extends DataStore { constructor(guild, iterable) { - super(guild.client, iterable); + super(guild.client, iterable, GuildMember); this.guild = guild; } - create(data, cache = true) { - const existing = this.get(data.user.id); - if (existing) return existing; + create(data, cache) { + return super.create(data, cache, { extras: [this.guild] }); + } - const member = new GuildMember(this.guild, data); - if (cache) this.set(member.id, member); + /** + * Data that resolves to give a GuildMember object. This can be: + * * A GuildMember object + * * A User resolvable + * @typedef {GuildMember|UserResolveable} GuildMemberResolvable + */ - return member; + /** + * Resolves a GuildMemberResolvable to a GuildMember object. + * @param {GuildMemberResolvable} member The user that is part of the guild + * @returns {?GuildMember} + */ + resolve(member) { + const memberResolveable = super.resolve(member); + if (memberResolveable) return memberResolveable; + const userResolveable = this.client.users.resolveID(member); + if (userResolveable) return super.resolve(userResolveable); + return null; + } + + /** + * Resolves a GuildMemberResolvable to an member ID string. + * @param {GuildMemberResolvable} member The user that is part of the guild + * @returns {?string} + */ + resolveID(member) { + const memberResolveable = super.resolveID(member); + if (memberResolveable) return memberResolveable; + const userResolveable = this.client.users.resolveID(member); + return this.has(userResolveable) ? userResolveable : null; } /** @@ -64,10 +90,10 @@ class GuildMemberStore extends DataStore { */ fetch(options) { if (!options) return this._fetchMany(); - const user = this.client.resolver.resolveUserID(options); + const user = this.resolveID(options); if (user) return this._fetchSingle({ user, cache: true }); if (options.user) { - options.user = this.client.resolver.resolveUserID(options.user); + options.user = this.resolveID(options.user); if (options.user) return this._fetchSingle(options); } return this._fetchMany(options); diff --git a/src/stores/GuildStore.js b/src/stores/GuildStore.js index 45201abb8..00d625700 100644 --- a/src/stores/GuildStore.js +++ b/src/stores/GuildStore.js @@ -1,20 +1,38 @@ const DataStore = require('./DataStore'); const Guild = require('../structures/Guild'); + /** * Stores guilds. * @private * @extends {DataStore} */ class GuildStore extends DataStore { - create(data, cache = true) { - const existing = this.get(data.id); - if (existing) return existing; - - const guild = new Guild(this.client, data); - if (cache) this.set(guild.id, guild); - - return guild; + constructor(client, iterable) { + super(client, iterable, Guild); } + + /** + * Data that resolves to give a Guild object. This can be: + * * A Guild object + * * A Snowflake + * @typedef {Guild|Snowflake} GuildResolvable + */ + + /** + * Resolves a GuildResolvable to a Guild object. + * @method resolve + * @memberof GuildStore + * @param {GuildResolvable} guild The guild resolvable to identify + * @returns {?Guild} + */ + + /** + * Resolves a GuildResolvable to a Guild ID string. + * @method resolveID + * @memberof GuildStore + * @param {GuildResolvable} guild The guild resolvable to identify + * @returns {?string} + */ } module.exports = GuildStore; diff --git a/src/stores/MessageStore.js b/src/stores/MessageStore.js index dca4345b4..c00929ce0 100644 --- a/src/stores/MessageStore.js +++ b/src/stores/MessageStore.js @@ -1,7 +1,7 @@ const DataStore = require('./DataStore'); const Collection = require('../util/Collection'); +const Message = require('../structures/Message'); const { Error } = require('../errors'); -let Message; /** * Stores messages for text-based channels. @@ -9,19 +9,12 @@ let Message; */ class MessageStore extends DataStore { constructor(channel, iterable) { - super(channel.client, iterable); + super(channel.client, iterable, Message); this.channel = channel; - Message = require('../structures/Message'); } - create(data, cache = true) { - const existing = this.get(data.id); - if (existing) return existing; - - const message = new Message(this.client.channels.get(data.channel_id), data, this.client); - - if (cache) this.set(message.id, message); - return message; + create(data, cache) { + return super.create(data, cache, { extras: [this.channel] }); } set(key, value) { @@ -62,7 +55,7 @@ class MessageStore extends DataStore { /** * Fetches the pinned messages of this channel and returns a collection of them. - * The returned Collection does not contain the reactions of the messages. + * The returned Collection does not contain the reactions of the messages. * Those need to be fetched seperately. * @returns {Promise>} */ @@ -95,6 +88,30 @@ class MessageStore extends DataStore { 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 + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Message} + */ + + /** + * Resolves a MessageResolvable to a Message ID string. + * @method MessageStore + * @memberof PresenceStore + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?string} + */ } module.exports = MessageStore; diff --git a/src/stores/PresenceStore.js b/src/stores/PresenceStore.js index 69569b65b..1877528d0 100644 --- a/src/stores/PresenceStore.js +++ b/src/stores/PresenceStore.js @@ -2,13 +2,46 @@ const DataStore = require('./DataStore'); const { Presence } = require('../structures/Presence'); class PresenceStore extends DataStore { - create(data) { - if (this.has(data.user.id)) { - this.get(data.user.id).patch(data); - } else { - this.set(data.user.id, new Presence(this.client, data)); - } - return this.get(data.user.id); + constructor(client, iterable) { + super(client, iterable, Presence); + } + + create(data, cache) { + const existing = this.get(data.user.id); + return existing ? existing.patch(data) : super.create(data, cache, { id: data.user.id }); + } + + /** + * Data that can be resolved to a Presence object. This can be: + * * A Presence + * * A UserResolveable + * * A Snowflake + * @typedef {Presence|UserResolveable|Snowflake} PresenceResolvable + */ + + /** + * Resolves a PresenceResolvable to a Presence object. + * @param {PresenceResolvable} presence The presence resolvable to resolve + * @returns {?Presence} + */ + resolve(presence) { + const presenceResolveable = super.resolve(presence); + if (presenceResolveable) return presenceResolveable; + const UserResolveable = this.client.users.resolveID(presence); + return super.resolve(UserResolveable) || null; + } + + + /** + * Resolves a PresenceResolvable to a Presence ID string. + * @param {PresenceResolvable} presence The presence resolvable to resolve + * @returns {?string} + */ + resolveID(presence) { + const presenceResolveable = super.resolveID(presence); + if (presenceResolveable) return presenceResolveable; + const userResolveable = this.client.users.resolveID(presence); + return this.has(userResolveable) ? userResolveable : null; } } diff --git a/src/stores/ReactionStore.js b/src/stores/ReactionStore.js index 276b69e5a..e096e9789 100644 --- a/src/stores/ReactionStore.js +++ b/src/stores/ReactionStore.js @@ -1,5 +1,6 @@ const DataStore = require('./DataStore'); const MessageReaction = require('../structures/MessageReaction'); + /** * Stores reactions. * @private @@ -7,21 +8,37 @@ const MessageReaction = require('../structures/MessageReaction'); */ class ReactionStore extends DataStore { constructor(message, iterable) { - super(message.client, iterable); + super(message.client, iterable, MessageReaction); this.message = message; } - create(data) { - const emojiID = data.emoji.id || decodeURIComponent(data.emoji.name); - - const existing = this.get(emojiID); - if (existing) return existing; - - const reaction = new MessageReaction(this.message, data.emoji, data.count, data.me); - this.set(emojiID, reaction); - - return reaction; + create(data, cache) { + data.emoji.id = data.emoji.id || decodeURIComponent(data.emoji.name); + return super.create(data, cache, { id: data.emoji.id, extras: [this.message] }); } + + /** + * Data that can be resolved to a MessageReaction object. This can be: + * * A MessageReaction + * * A Snowflake + * @typedef {MessageReaction|Snowflake} MessageReactionResolvable + */ + + /** + * Resolves a MessageReactionResolvable to a MessageReaction object. + * @method resolve + * @memberof ReactionStore + * @param {MessageReactionResolvable} reaction The MessageReaction to resolve + * @returns {?MessageReaction} + */ + + /** + * Resolves a MessageReactionResolvable to a MessageReaction ID string. + * @method resolveID + * @memberof ReactionStore + * @param {MessageReactionResolvable} role The role resolvable to resolve + * @returns {?string} + */ } module.exports = ReactionStore; diff --git a/src/stores/RoleStore.js b/src/stores/RoleStore.js index 068ad5ec3..c6bb9402c 100644 --- a/src/stores/RoleStore.js +++ b/src/stores/RoleStore.js @@ -1,5 +1,6 @@ const DataStore = require('./DataStore'); const Role = require('../structures/Role'); + /** * Stores roles. * @private @@ -7,19 +8,36 @@ const Role = require('../structures/Role'); */ class RoleStore extends DataStore { constructor(guild, iterable) { - super(guild.client, iterable); + super(guild.client, iterable, Role); this.guild = guild; } - create(data) { - const existing = this.get(data.id); - if (existing) return existing; - - const role = new Role(this.guild, data); - this.set(role.id, role); - - return role; + create(data, cache) { + return super.create(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 + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Role} + */ + + /** + * Resolves a RoleResolvable to a role ID string. + * @method resolveID + * @memberof RoleStore + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?string} + */ } module.exports = RoleStore; diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index 2eab91d01..6ee4909a3 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -1,18 +1,46 @@ const DataStore = require('./DataStore'); const User = require('../structures/User'); +const GuildMember = require('../structures/GuildMember'); +const Message = require('../structures/Message'); /** * A data store to store User models. * @extends {DataStore} */ class UserStore extends DataStore { - create(data, cache = true) { - const existing = this.get(data.id); - if (existing) return existing; + constructor(client, iterable) { + super(client, iterable, User); + } - const user = new User(this.client, data); - if (cache) this.set(user.id, user); - return user; + /** + * Data that resolves to give a User object. This can be: + * * A User object + * * A Snowflake + * * A Message object (resolves to the message author) + * * A GuildMember object + * @typedef {User|Snowflake|Message|GuildMember} UserResolvable + */ + + /** + * Resolves a UserResolvable to a User object. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?User} + */ + resolve(user) { + if (user instanceof GuildMember) return user.user; + if (user instanceof Message) return user.author; + return super.resolve(user); + } + + /** + * Resolves a UserResolvable to a user ID string. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?string} + */ + resolveID(user) { + if (user instanceof GuildMember) return user.user.id; + if (user instanceof Message) return user.author.id; + return super.resolveID(user); } /** diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 27e309597..84fea5521 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -1,5 +1,6 @@ const Snowflake = require('../util/Snowflake'); const Constants = require('../util/Constants'); +const DataResolver = require('../util/DataResolver'); const Base = require('./Base'); /** @@ -165,7 +166,7 @@ class ClientApplication extends Base { * @returns {Promise} */ createAsset(name, data, type) { - return this.client.resolveBase64(data).then(b64 => + return DataResolver.resolveBase64(data).then(b64 => this.client.api.applications(this.id).assets.post({ data: { name, data: b64, diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 724d4b819..7c70261c6 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -4,6 +4,7 @@ const ClientUserSettings = require('./ClientUserSettings'); const ClientUserGuildSettings = require('./ClientUserGuildSettings'); const Constants = require('../util/Constants'); const Util = require('../util/Util'); +const DataResolver = require('../util/DataResolver'); const Guild = require('./Guild'); /** @@ -177,7 +178,7 @@ class ClientUser extends User { * .catch(console.error); */ async setAvatar(avatar) { - return this.edit({ avatar: await this.client.resolver.resolveImage(avatar) }); + return this.edit({ avatar: await DataResolver.resolveImage(avatar, this.client.browser) }); } /** @@ -293,7 +294,7 @@ class ClientUser extends User { ); } - return this.client.resolver.resolveImage(icon) + return DataResolver.resolveImage(icon, this.client.browser) .then(data => this.createGuild(name, { region, icon: data || null })); } @@ -320,7 +321,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.user || u.id)) }; + } : { 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.create(res)); } diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js index d46766a83..b912da6f2 100644 --- a/src/structures/Emoji.js +++ b/src/structures/Emoji.js @@ -8,8 +8,8 @@ const { TypeError } = require('../errors'); * @extends {Base} */ class Emoji extends Base { - constructor(guild, data) { - super(guild.client); + constructor(client, data, guild) { + super(client); /** * The guild this emoji is part of @@ -152,7 +152,7 @@ class Emoji extends Base { addRestrictedRoles(roles) { const newRoles = new Collection(this.roles); for (let role of roles instanceof Collection ? roles.values() : roles) { - role = this.client.resolver.resolveRole(this.guild, role); + role = this.guild.roles.resolve(role); if (!role) { return Promise.reject(new TypeError('INVALID_TYPE', 'roles', 'Array or Collection of Roles or Snowflakes', true)); @@ -179,7 +179,7 @@ class Emoji extends Base { removeRestrictedRoles(roles) { const newRoles = new Collection(this.roles); for (let role of roles instanceof Collection ? roles.values() : roles) { - role = this.client.resolver.resolveRole(this.guild, role); + role = this.guild.roles.resolve(role); if (!role) { return Promise.reject(new TypeError('INVALID_TYPE', 'roles', 'Array or Collection of Roles or Snowflakes', true)); diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 20cef1064..06ba2f946 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 DataResolver = require('../util/DataResolver'); const MessageStore = require('../stores/MessageStore'); /* @@ -160,7 +161,7 @@ class GroupDMChannel extends Channel { * @returns {Promise} */ async setIcon(icon) { - return this.edit({ icon: await this.client.resolver.resolveImage(icon) }); + return this.edit({ icon: await DataResolver.resolveImage(icon, this.client.browser) }); } /** @@ -182,7 +183,7 @@ class GroupDMChannel extends Channel { * @returns {Promise} */ addUser({ user, accessToken, nick }) { - const id = this.client.resolver.resolveUserID(user); + const id = this.client.users.resolveID(user); const data = this.client.user.bot ? { nick, access_token: accessToken } : { recipient: id }; @@ -196,7 +197,7 @@ class GroupDMChannel extends Channel { * @returns {Promise} */ removeUser(user) { - const id = this.client.resolver.resolveUserID(user); + const id = this.client.users.resolveID(user); return this.client.api.channels[this.id].recipients[id].delete() .then(() => this); } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 83715a492..9c70db67a 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -9,6 +9,7 @@ const VoiceRegion = require('./VoiceRegion'); const Constants = require('../util/Constants'); const Collection = require('../util/Collection'); const Util = require('../util/Util'); +const DataResolver = require('../util/DataResolver'); const Snowflake = require('../util/Snowflake'); const Permissions = require('../util/Permissions'); const Shared = require('./shared'); @@ -431,7 +432,7 @@ class Guild extends Base { * const member = guild.member(message.author); */ member(user) { - return this.client.resolver.resolveGuildMember(this, user); + return this.members.resolve(user); } /** @@ -511,7 +512,7 @@ class Guild extends Base { before: options.before, after: options.after, limit: options.limit, - user_id: this.client.resolver.resolveUserID(options.user), + user_id: this.client.users.resolveID(options.user), action_type: options.type, } }) .then(data => GuildAuditLogs.build(this, data)); @@ -536,7 +537,7 @@ class Guild extends Base { if (options.roles) { const roles = []; for (let role of options.roles instanceof Collection ? options.roles.values() : options.roles) { - role = this.client.resolver.resolveRole(this, role); + role = this.roles.resolve(role); if (!role) { return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true)); @@ -601,14 +602,14 @@ class Guild extends Base { if (data.region) _data.region = data.region; if (typeof data.verificationLevel !== 'undefined') _data.verification_level = Number(data.verificationLevel); if (typeof data.afkChannel !== 'undefined') { - _data.afk_channel_id = this.client.resolver.resolveChannelID(data.afkChannel); + _data.afk_channel_id = this.client.channels.resolveID(data.afkChannel); } if (typeof data.systemChannel !== 'undefined') { - _data.system_channel_id = this.client.resolver.resolveChannelID(data.systemChannel); + _data.system_channel_id = this.client.channels.resolveID(data.systemChannel); } 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.resolver.resolveUser(data.owner).id; + if (data.owner) _data.owner_id = this.client.users.resolve(data.owner).id; if (data.splash) _data.splash = data.splash; if (typeof data.explicitContentFilter !== 'undefined') { _data.explicit_content_filter = Number(data.explicitContentFilter); @@ -724,7 +725,7 @@ class Guild extends Base { * .catch(console.error); */ async setIcon(icon, reason) { - return this.edit({ icon: await this.client.resolver.resolveImage(icon), reason }); + return this.edit({ icon: await DataResolver.resolveImage(icon, this.client.browser), reason }); } /** @@ -754,7 +755,7 @@ class Guild extends Base { * .catch(console.error); */ async setSplash(splash, reason) { - return this.edit({ splash: await this.client.resolver.resolveImage(splash), reason }); + return this.edit({ splash: await DataResolver.resolveImage(splash, this.client.browser), reason }); } /** @@ -815,14 +816,14 @@ class Guild extends Base { */ ban(user, options = { days: 0 }) { if (options.days) options['delete-message-days'] = options.days; - const id = this.client.resolver.resolveUserID(user); + const id = this.client.users.resolveID(user); 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; - const _user = this.client.resolver.resolveUser(id); + const _user = this.client.users.resolve(id); if (_user) { - const member = this.client.resolver.resolveGuildMember(this, _user); + const member = this.members.resolve(_user); return member || _user; } return id; @@ -841,7 +842,7 @@ class Guild extends Base { * .catch(console.error); */ unban(user, reason) { - const id = this.client.resolver.resolveUserID(user); + const id = this.client.users.resolverID(user); if (!id) throw new Error('BAN_RESOLVE_ID'); return this.client.api.guilds(this.id).bans[id].delete({ reason }) .then(() => user); @@ -910,12 +911,12 @@ class Guild extends Base { 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); + const role = this.roles.resolve(overwrite.id); if (role) { overwrite.id = role.id; overwrite.type = 'role'; } else { - overwrite.id = this.client.resolver.resolveUserID(overwrite.id); + overwrite.id = this.client.users.resolveID(overwrite.id); overwrite.type = 'member'; } @@ -988,7 +989,7 @@ class Guild extends Base { */ setChannelPositions(channelPositions) { const updatedChannels = channelPositions.map(r => ({ - id: this.client.resolver.resolveChannelID(r.channel), + id: this.client.channels.resolveID(r.channel), position: r.position, })); @@ -1063,7 +1064,7 @@ class Guild extends Base { if (roles) { data.roles = []; for (let role of roles instanceof Collection ? roles.values() : roles) { - role = this.client.resolver.resolveRole(this, role); + role = this.roles.resolve(role); if (!role) { return Promise.reject(new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true)); @@ -1076,7 +1077,7 @@ class Guild extends Base { .then(emoji => this.client.actions.GuildEmojiCreate.handle(this, emoji).emoji); } - return this.client.resolver.resolveImage(attachment) + return DataResolver.resolveImage(attachment, this.client.browser) .then(image => this.createEmoji(image, name, { roles, reason })); } diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 8a3ce714a..195b40053 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -66,7 +66,7 @@ class GuildChannel extends Channel { * @returns {?Permissions} */ permissionsFor(member) { - member = this.client.resolver.resolveGuildMember(this.guild, member); + member = this.guild.members.resolve(member); if (!member) return null; if (member.id === this.guild.ownerID) return new Permissions(Permissions.ALL); @@ -101,7 +101,7 @@ class GuildChannel extends Channel { } overwritesFor(member, verified = false, roles = null) { - if (!verified) member = this.client.resolver.resolveGuildMember(this.guild, member); + if (!verified) member = this.guild.members.resolve(member); if (!member) return []; roles = roles || member.roles; @@ -158,13 +158,13 @@ class GuildChannel extends Channel { deny: 0, }; - if (userOrRole instanceof Role) { - payload.type = 'role'; - } else if (this.guild.roles.has(userOrRole)) { - userOrRole = this.guild.roles.get(userOrRole); + const role = this.guild.roles.get(userOrRole); + + if (role || userOrRole instanceof Role) { + userOrRole = role || userOrRole; payload.type = 'role'; } else { - userOrRole = this.client.resolver.resolveUser(userOrRole); + userOrRole = this.client.users.resolve(userOrRole); payload.type = 'member'; if (!userOrRole) return Promise.reject(new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role', true)); } diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 420208cb3..1462f8da8 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -12,8 +12,8 @@ const { Error, TypeError } = require('../errors'); * @extends {Base} */ class GuildMember extends Base { - constructor(guild, data) { - super(guild.client); + constructor(client, data, guild) { + super(client); /** * The guild that this member is part of @@ -291,7 +291,7 @@ class GuildMember extends Base { * @returns {?Permissions} */ permissionsIn(channel) { - channel = this.client.resolver.resolveChannel(channel); + channel = this.client.channels.resolve(channel); if (!channel || !channel.guild) throw new Error('GUILD_CHANNEL_RESOLVE'); return channel.permissionsFor(this); } @@ -342,7 +342,7 @@ class GuildMember extends Base { */ edit(data, reason) { if (data.channel) { - data.channel_id = this.client.resolver.resolveChannel(data.channel).id; + data.channel_id = this.client.channels.resolve(data.channel).id; data.channel = null; } if (data.roles) data.roles = data.roles.map(role => role instanceof Role ? role.id : role); @@ -412,7 +412,7 @@ class GuildMember extends Base { * @returns {Promise} */ addRole(role, reason) { - role = this.client.resolver.resolveRole(this.guild, role); + role = this.guild.roles.resolve(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) @@ -433,7 +433,7 @@ class GuildMember extends Base { addRoles(roles, reason) { let allRoles = this._roles.slice(); for (let role of roles instanceof Collection ? roles.values() : roles) { - role = this.client.resolver.resolveRole(this.guild, role); + role = this.guild.roles.resolve(role); if (!role) { return Promise.reject(new TypeError('INVALID_TYPE', 'roles', 'Array or Collection of Roles or Snowflakes', true)); @@ -450,7 +450,7 @@ class GuildMember extends Base { * @returns {Promise} */ removeRole(role, reason) { - role = this.client.resolver.resolveRole(this.guild, role); + role = this.guild.roles.resolve(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) @@ -472,7 +472,7 @@ class GuildMember extends Base { removeRoles(roles, reason) { const allRoles = this._roles.slice(); for (let role of roles instanceof Collection ? roles.values() : roles) { - role = this.client.resolver.resolveRole(this.guild, role); + role = this.guild.roles.resolve(role); if (!role) { return Promise.reject(new TypeError('INVALID_TYPE', 'roles', 'Array or Collection of Roles or Snowflakes', true)); diff --git a/src/structures/Message.js b/src/structures/Message.js index 96dab137b..58fc9d26f 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -17,7 +17,7 @@ let GuildMember; * @extends {Base} */ class Message extends Base { - constructor(channel, data, client) { + constructor(client, data, channel) { super(client); /** @@ -393,7 +393,7 @@ class Message extends Base { // Add the reply prefix if (reply && this.channel.type !== 'dm') { - const id = this.client.resolver.resolveUserID(reply); + const id = this.client.users.resolveID(reply); const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; content = `${mention}${content ? `, ${content}` : ''}`; } @@ -427,11 +427,11 @@ class Message extends Base { /** * Add a reaction to the message. - * @param {string|Emoji|ReactionEmoji} emoji The emoji to react with + * @param {EmojiIdentifierResolveable} emoji The emoji to react with * @returns {Promise} */ react(emoji) { - emoji = this.client.resolver.resolveEmojiIdentifier(emoji); + emoji = this.client.emojis.resolveIdentifier(emoji); if (!emoji) throw new TypeError('EMOJI_TYPE'); return this.client.api.channels(this.channel.id).messages(this.id).reactions(emoji, '@me') diff --git a/src/structures/MessageReaction.js b/src/structures/MessageReaction.js index bf3235b10..a55550fee 100644 --- a/src/structures/MessageReaction.js +++ b/src/structures/MessageReaction.js @@ -7,7 +7,7 @@ const { Error } = require('../errors'); * Represents a reaction to a message. */ class MessageReaction { - constructor(message, emoji, count, me) { + constructor(client, data, message) { /** * The message that this reaction refers to * @type {Message} @@ -18,13 +18,13 @@ class MessageReaction { * Whether the client has given this reaction * @type {boolean} */ - this.me = me; + this.me = data.me; /** * The number of people that have given the same reaction * @type {number} */ - this.count = count || 0; + this.count = data.count || 0; /** * The users that have given this reaction, mapped by their ID @@ -32,7 +32,7 @@ class MessageReaction { */ this.users = new Collection(); - this._emoji = new ReactionEmoji(this, emoji.name, emoji.id); + this._emoji = new ReactionEmoji(this, data.emoji.name, data.emoji.id); } /** @@ -62,7 +62,7 @@ class MessageReaction { * @returns {Promise} */ remove(user = this.message.client.user) { - const userID = this.message.client.resolver.resolveUserID(user); + const userID = this.message.client.users.resolveID(user); 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] diff --git a/src/structures/Presence.js b/src/structures/Presence.js index 38b574e83..cb521eeb5 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -26,6 +26,8 @@ class Presence { * @type {?Activity} */ this.activity = activity ? new Activity(this, activity) : null; + + return this; } _clone() { diff --git a/src/structures/Role.js b/src/structures/Role.js index 486e7a09c..a834f50d7 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -9,8 +9,8 @@ const { TypeError } = require('../errors'); * @extends {Base} */ class Role extends Base { - constructor(guild, data) { - super(guild.client); + constructor(client, data, guild) { + super(client); /** * The guild that the role belongs to @@ -171,7 +171,7 @@ class Role extends Base { * 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); + role = this.guild.roles.resolve(role); if (!role) return Promise.reject(new TypeError('INVALID_TYPE', 'role', 'Role nor a Snowflake')); return this.constructor.comparePositions(this, role); } diff --git a/src/structures/TextChannel.js b/src/structures/TextChannel.js index 071a72c21..2c536045a 100644 --- a/src/structures/TextChannel.js +++ b/src/structures/TextChannel.js @@ -2,6 +2,7 @@ const GuildChannel = require('./GuildChannel'); const Webhook = require('./Webhook'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const Collection = require('../util/Collection'); +const DataResolver = require('../util/DataResolver'); const MessageStore = require('../stores/MessageStore'); /** @@ -63,7 +64,7 @@ class TextChannel extends GuildChannel { */ async createWebhook(name, { avatar, reason } = {}) { if (typeof avatar === 'string' && !avatar.startsWith('data:')) { - avatar = await this.client.resolver.resolveImage(avatar); + avatar = await DataResolver.resolveImage(avatar, this.client.browser); } return this.client.api.channels[this.id].webhooks.post({ data: { name, avatar, diff --git a/src/structures/User.js b/src/structures/User.js index 3e50b23b7..fa15c6c3d 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -158,7 +158,7 @@ class User extends Base { * @returns {boolean} */ typingIn(channel) { - channel = this.client.resolver.resolveChannel(channel); + channel = this.client.channels.resolve(channel); return channel._typing.has(this.id); } @@ -168,7 +168,7 @@ class User extends Base { * @returns {?Date} */ typingSinceIn(channel) { - channel = this.client.resolver.resolveChannel(channel); + channel = this.client.channels.resolve(channel); return channel._typing.has(this.id) ? new Date(channel._typing.get(this.id).since) : null; } @@ -178,7 +178,7 @@ class User extends Base { * @returns {number} */ typingDurationIn(channel) { - channel = this.client.resolver.resolveChannel(channel); + channel = this.client.channels.resolve(channel); return channel._typing.has(this.id) ? channel._typing.get(this.id).elapsedTime : -1; } diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index fd2c633a4..a163a06b4 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -1,5 +1,6 @@ const path = require('path'); const Util = require('../util/Util'); +const DataResolver = require('../util/DataResolver'); const Embed = require('./MessageEmbed'); const MessageAttachment = require('./MessageAttachment'); const MessageEmbed = require('./MessageEmbed'); @@ -168,7 +169,7 @@ class Webhook { } return Promise.all(options.files.map(file => - this.client.resolver.resolveFile(file.attachment).then(resource => { + DataResolver.resolveFile(file.attachment, this.client.browser).then(resource => { file.file = resource; return file; }) @@ -248,7 +249,7 @@ class Webhook { */ edit({ name = this.name, avatar }, reason) { if (avatar && (typeof avatar === 'string' && !avatar.startsWith('data:'))) { - return this.client.resolver.resolveImage(avatar).then(image => + return DataResolver.resolveImage(avatar, this.client.browser).then(image => this.edit({ name, avatar: image }, reason) ); } diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index f444f3970..4293769f0 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,9 +1,9 @@ const path = require('path'); const MessageCollector = require('../MessageCollector'); const Shared = require('../shared'); -const MessageStore = require('../../stores/MessageStore'); const Snowflake = require('../../util/Snowflake'); const Collection = require('../../util/Collection'); +const DataResolver = require('../../util/DataResolver'); const MessageAttachment = require('../../structures/MessageAttachment'); const MessageEmbed = require('../../structures/MessageEmbed'); const { RangeError, TypeError } = require('../../errors'); @@ -124,7 +124,7 @@ class TextBasedChannel { } return Promise.all(options.files.map(file => - this.client.resolver.resolveFile(file.attachment).then(resource => { + DataResolver.resolveFile(file.attachment, this.client.browser).then(resource => { file.file = resource; return file; }) @@ -341,3 +341,6 @@ class TextBasedChannel { } module.exports = TextBasedChannel; + +// Fixes Circular +const MessageStore = require('../../stores/MessageStore'); diff --git a/src/structures/shared/Search.js b/src/structures/shared/Search.js index b039239ad..971f2483d 100644 --- a/src/structures/shared/Search.js +++ b/src/structures/shared/Search.js @@ -52,9 +52,9 @@ module.exports = function search(target, options) { options.minID = long.fromNumber(t).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.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; } diff --git a/src/structures/shared/SendMessage.js b/src/structures/shared/SendMessage.js index 48212295a..560ece728 100644 --- a/src/structures/shared/SendMessage.js +++ b/src/structures/shared/SendMessage.js @@ -17,7 +17,7 @@ module.exports = function sendMessage(channel, options) { // eslint-disable-line // Add the reply prefix if (reply && !(channel instanceof User || channel instanceof GuildMember) && channel.type !== 'dm') { - const id = channel.client.resolver.resolveUserID(reply); + const id = channel.client.users.resolveID(reply); const mention = `<@${reply instanceof GuildMember && reply.nickname ? '!' : ''}${id}>`; if (split) split.prepend = `${mention}, ${split.prepend || ''}`; content = `${mention}${typeof content !== 'undefined' ? `, ${content}` : ''}`; diff --git a/src/util/DataResolver.js b/src/util/DataResolver.js new file mode 100644 index 000000000..065badf2e --- /dev/null +++ b/src/util/DataResolver.js @@ -0,0 +1,124 @@ +const path = require('path'); +const fs = require('fs'); +const snekfetch = require('snekfetch'); +const Util = require('../util/Util'); +const { Error, TypeError } = require('../errors'); + +/** + * The DataResolver identifies different objects and tries to resolve a specific piece of information from them. + * @private + */ +class DataResolver { + constructor() { + throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + } + + /** + * Data that can be resolved to give an invite code. This can be: + * * An invite code + * * An invite URL + * @typedef {string} InviteResolvable + */ + + /** + * Resolves InviteResolvable to an invite code. + * @param {InviteResolvable} data The invite resolvable to resolve + * @returns {string} + */ + static resolveInviteCode(data) { + const inviteRegex = /discord(?:app\.com\/invite|\.gg)\/([\w-]{2,255})/i; + const match = inviteRegex.exec(data); + if (match && match[1]) return match[1]; + return data; + } + + /** + * Resolves a Base64Resolvable, a string, or a BufferResolvable to a Base 64 image. + * @param {BufferResolvable|Base64Resolvable} image The image to be resolved + * @param {boolean} browser Whether this should resolve for a browser + * @returns {Promise} + */ + static async resolveImage(image, browser) { + if (!image) return null; + if (typeof image === 'string' && image.startsWith('data:')) { + return image; + } + const file = await this.resolveFile(image, browser); + return this.constructor.resolveBase64(file); + } + + /** + * Data that resolves to give a Base64 string, typically for image uploading. This can be: + * * A Buffer + * * A base64 string + * @typedef {Buffer|string} Base64Resolvable + */ + + /** + * Resolves a Base64Resolvable to a Base 64 image. + * @param {Base64Resolvable} data The base 64 resolvable you want to resolve + * @returns {?string} + */ + static resolveBase64(data) { + if (data instanceof Buffer) return `data:image/jpg;base64,${data.toString('base64')}`; + return data; + } + + /** + * Data that can be resolved to give a Buffer. This can be: + * * A Buffer + * * The path to a local file + * * A URL + * @typedef {string|Buffer} BufferResolvable + */ + + /** + * @external Stream + * @see {@link https://nodejs.org/api/stream.html} + */ + + /** + * Resolves a BufferResolvable to a Buffer. + * @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve + * @param {boolean} browser Whether this should resolve for a browser + * @returns {Promise} + */ + static resolveFile(resource, browser) { + if (resource instanceof Buffer) return Promise.resolve(resource); + if (browser && resource instanceof ArrayBuffer) return Promise.resolve(Util.convertToBuffer(resource)); + + if (typeof resource === 'string') { + return new Promise((resolve, reject) => { + if (/^https?:\/\//.test(resource)) { + snekfetch.get(resource) + .end((err, res) => { + if (err) return reject(err); + 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('FILE_NOT_FOUND', file)); + fs.readFile(file, (err2, data) => { + if (err2) reject(err2); else resolve(data); + }); + return null; + }); + } + }); + } 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')); + } +} + +module.exports = DataResolver;