From fa016b6b4178d18bf094646088a3d9f2d8c54a4d Mon Sep 17 00:00:00 2001 From: Schuyler Cebulskie Date: Mon, 10 Apr 2017 03:01:50 -0400 Subject: [PATCH] Clean up Message#mentions and message updates --- src/client/actions/MessageUpdate.js | 35 +------ src/structures/Message.js | 103 +++++++++----------- src/structures/MessageMentions.js | 145 ++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 89 deletions(-) create mode 100644 src/structures/MessageMentions.js diff --git a/src/client/actions/MessageUpdate.js b/src/client/actions/MessageUpdate.js index 521d0cce1..48bd692ba 100644 --- a/src/client/actions/MessageUpdate.js +++ b/src/client/actions/MessageUpdate.js @@ -1,6 +1,5 @@ const Action = require('./Action'); const Constants = require('../../util/Constants'); -const Message = require('../../structures/Message'); class MessageUpdateAction extends Action { handle(data) { @@ -10,14 +9,11 @@ class MessageUpdateAction extends Action { if (channel) { const message = channel.messages.get(data.id); if (message) { - const newMessage = new Message(message.channel, this.patchDataPacket(data, message), client); - newMessage._edits.push(message, ...message._edits); - newMessage.reactions = message.reactions; - channel.messages.set(data.id, newMessage); - client.emit(Constants.Events.MESSAGE_UPDATE, message, newMessage); + message.patch(data); + client.emit(Constants.Events.MESSAGE_UPDATE, message._edits[0], message); return { - old: message, - updated: newMessage, + old: message._edits[0], + updated: message, }; } @@ -32,29 +28,6 @@ class MessageUpdateAction extends Action { updated: null, }; } - - patchDataPacket(data, message) { - data.type = 'type' in data ? data.type : Constants.MessageTypes.indexOf(message.type); - data.tts = 'tts' in data ? data.tts : message.tts; - data.timestamp = 'timestamp' in data ? data.timestamp : message.createdAt.toString(); - data.pinned = 'pinned' in data ? data.pinned : message.pinned; - data.nonce = 'nonce' in data ? data.nonce : message.nonce; - data.mentions = 'mentions' in data ? data.mentions : message.mentions.users.keyArray(); - data.mentions_roles = 'mentions_roles' in data ? - data.mentions_roles : message.mentions.roles.keyArray(); - data.mention_everyone = 'mention_everyone' in data ? data.mention_everyone : message.mentions.everyone; - data.embeds = 'embeds' in data ? data.embeds : message.embeds; - data.content = 'content' in data ? data.content : message.content; - data.author = 'author' in data ? data.author : { - username: message.author.username, - id: message.author.id, - discriminator: message.author.discriminator, - avatar: message.author.avatar, - }; - data.attachments = 'attachments' in data ? data.attachments : message.attachments.array(); - return data; - } - } /** diff --git a/src/structures/Message.js b/src/structures/Message.js index 3e7956769..0cc20251b 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -1,3 +1,4 @@ +const Mentions = require('./MessageMentions'); const Attachment = require('./MessageAttachment'); const Embed = require('./MessageEmbed'); const MessageReaction = require('./MessageReaction'); @@ -110,69 +111,11 @@ class Message { */ this.editedTimestamp = data.edited_timestamp ? new Date(data.edited_timestamp).getTime() : null; - /** - * An object containing a further users, roles or channels collections - * @type {Object} - * @property {Collection} mentions.users Mentioned users, maps their ID to the user object. - * @property {Collection} mentions.members Mentioned members, maps their ID - * to the member object. - * @property {Collection} mentions.roles Mentioned roles, maps their ID to the role object. - * @property {Collection} mentions.channels Mentioned channels, - * maps their ID to the channel object. - * @property {boolean} mentions.everyone Whether or not @everyone was mentioned. - */ - this.mentions = { - users: new Collection(), - roles: new Collection(), - channels: new Collection(), - everyone: data.mention_everyone, - }; - - // Add user mentions - for (const mention of data.mentions) { - let user = this.client.users.get(mention.id); - if (!user) user = this.client.dataManager.newUser(mention); - this.mentions.users.set(user.id, user); - } - - // Add getter for member mentions - Object.defineProperty(this.mentions, 'members', { - get: () => { - if (this.channel.type !== 'text') return null; - const members = new Collection(); - this.mentions.users.forEach(user => { - const member = this.client.resolver.resolveGuildMember(this.channel.guild, user); - if (member) members.set(member.id, member); - }); - return members; - }, - }); - - // Add role mentions - if (data.mention_roles) { - for (const mention of data.mention_roles) { - const role = this.channel.guild.roles.get(mention); - if (role) this.mentions.roles.set(role.id, role); - } - } - - // Add channel mentions - if (this.channel.type === 'text') { - const channMentionsRaw = data.content.match(/<#([0-9]{14,20})>/g) || []; - for (const raw of channMentionsRaw) { - const chan = this.channel.guild.channels.get(raw.match(/([0-9]{14,20})/g)[0]); - if (chan) this.mentions.channels.set(chan.id, chan); - } - } - - this._edits = []; - /** * A collection of reactions to this message, mapped by the reaction "id". * @type {Collection} */ this.reactions = new Collection(); - if (data.reactions && data.reactions.length > 0) { for (const reaction of data.reactions) { const id = reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name; @@ -180,6 +123,12 @@ class Message { } } + /** + * All valid mentions that the message contains + * @type {MessageMentions} + */ + this.mentions = new Mentions(this, data.mentions, data.mentions_roles, data.mention_everyone); + /** * ID of the webhook that sent the message, if applicable * @type {?Snowflake} @@ -191,6 +140,44 @@ class Message { * @type {?boolean} */ this.hit = typeof data.hit === 'boolean' ? data.hit : null; + + /** + * The previous versions of the message, sorted with the most recent first + * @type {Message[]} + * @private + */ + this._edits = []; + } + + /** + * Updates the message + * @param {Object} data Raw Discord message update data + * @private + */ + patch(data) { + const clone = Util.cloneObject(this); + this._edits.unshift(clone); + + this.editedTimestamp = data.edited_timestamp; + if ('content' in data) this.content = data.content; + if ('pinned' in data) this.pinned = data.pinned; + if ('tts' in data) this.tts = data.tts; + if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(this, e)); + else this.embeds = new Collection(this.embeds); + + if ('attachments' in data) { + this.attachments = new Collection(); + for (const attachment of data.attachments) this.attachments.set(attachment.id, new Attachment(this, attachment)); + } else { + this.attachments = new Collection(this.attachments); + } + + this.mentions = new Mentions( + this, + 'mentions' in data ? data.mentions : this.mentions.users, + 'mentions_roles' in data ? data.mentions_roles : this.mentions.roles, + 'mention_everyone' in data ? data.mention_everyone : this.mentions.everyone + ); } /** diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js new file mode 100644 index 000000000..9759388d4 --- /dev/null +++ b/src/structures/MessageMentions.js @@ -0,0 +1,145 @@ +const Collection = require('../util/Collection'); + +/** + * Keeps track of mentions in a {@link Message} + */ +class MessageMentions { + /** + * @param {Message} message Message to read mentions from + * @param {?Array} users Raw user objects from Discord + * @param {?Array} roles Raw role objects from Discord + * @param {?boolean} everyone Whether @everyone or @here were mentioned + * @private + */ + constructor(message, users, roles, everyone) { + /** + * Whether `@everyone` or ``@here` were mentioned + * @type {boolean} + */ + this.everyone = Boolean(everyone); + + if (users) { + if (users instanceof Collection) { + /** + * Any users that were mentioned + * @type {Collection} + */ + this.users = new Collection(users); + } else { + this.users = new Collection(); + for (const mention of users) { + let user = message.client.users.get(mention.id); + if (!user) user = message.client.dataManager.newUser(mention); + this.users.set(user.id, user); + } + } + } else { + this.users = new Collection(); + } + + if (roles) { + if (roles instanceof Collection) { + /** + * Any roles that were mentioned + * @type {Collection} + */ + this.roles = new Collection(roles); + } else { + this.roles = new Collection(); + for (const mention of roles) { + const role = message.channel.guild.roles.get(mention); + if (role) this.roles.set(role.id, role); + } + } + } else { + this.roles = new Collection(); + } + + /** + * Content of the message + * @type {Message} + * @private + */ + this._content = message.content; + + /** + * Guild the message is in + * @type {?Guild} + * @private + */ + this._guild = message.channel.guild; + + /** + * Cached members for {@MessageMention#members} + * @type {?Collection} + * @private + */ + this._members = null; + + /** + * Cached channels for {@MessageMention#channels} + * @type {?Collection} + * @private + */ + this._channels = null; + } + + /** + * Any members that were mentioned (only in {@link TextChannel}s) + * @type {?Collection} + * @readonly + */ + get members() { + if (this._members) return this._members; + if (!this.guild) return null; + this._members = new Collection(); + this.users.forEach(user => { + const member = this._guild.member(user); + if (member) this._members.set(member.user.id, member); + }); + return this._members; + } + + /** + * Any channels that were mentioned (only in {@link TextChannel}s) + * @type {?Collection} + * @readonly + */ + get channels() { + if (this._channels) return this._channels; + if (!this.guild) return null; + this._channels = new Collection(); + let matches; + while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) { + const chan = this._guild.channels.get(matches[1]); + if (chan) this._channels.set(chan.id, chan); + } + return this._channels; + } +} + +/** + * Regular expression that globally matches `@everyone` and `@here` + * @type {RegExp} + */ +MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g; + +/** + * Regular expression that globally matches user mentions like `<#81440962496172032>` + * @type {RegExp} + */ +MessageMentions.USERS_PATTERN = /<@!?[0-9]+>/g; + +/** + * Regular expression that globally matches role mentions like `<@&297577916114403338>` + * @type {RegExp} + */ +MessageMentions.ROLES_PATTERN = /<@&[0-9]+>/g; + +/** + * Regular expression that globally matches channel mentions like `<#222079895583457280>` + * @type {RegExp} + */ +MessageMentions.CHANNELS_PATTERN = /<#([0-9]+)>/g; + +module.exports = MessageMentions;