--[=[ @c Message x Snowflake @d Represents a text message sent in a Discord text channel. Messages can contain simple content strings, rich embeds, attachments, or reactions. ]=] local json = require('json') local enums = require('enums') local constants = require('constants') local Cache = require('iterables/Cache') local ArrayIterable = require('iterables/ArrayIterable') local Snowflake = require('containers/abstract/Snowflake') local Reaction = require('containers/Reaction') local Resolver = require('client/Resolver') local insert = table.insert local null = json.null local format = string.format local messageFlag = enums.messageFlag local band, bor, bnot = bit.band, bit.bor, bit.bnot local Message, get = require('class')('Message', Snowflake) function Message:__init(data, parent) Snowflake.__init(self, data, parent) self._author = self.client._users:_insert(data.author) if data.member then data.member.user = data.author self._parent._parent._members:_insert(data.member) end self._timestamp = nil -- waste of space; can be calculated from Snowflake ID if data.reactions and #data.reactions > 0 then self._reactions = Cache(data.reactions, Reaction, self) end return self:_loadMore(data) end function Message:_load(data) Snowflake._load(self, data) return self:_loadMore(data) end local function parseMentions(content, pattern) if not content:find('%b<>') then return end local mentions, seen = {}, {} for id in content:gmatch(pattern) do if not seen[id] then insert(mentions, id) seen[id] = true end end return mentions end function Message:_loadMore(data) local mentions = {} if data.mentions then for _, user in ipairs(data.mentions) do mentions[user.id] = true if user.member then user.member.user = user self._parent._parent._members:_insert(user.member) else self.client._users:_insert(user) end end end if data.referenced_message and data.referenced_message ~= null then if mentions[data.referenced_message.author.id] then self._reply_target = data.referenced_message.author.id end self._referencedMessage = self._parent._messages:_insert(data.referenced_message) end local content = data.content if content then if self._mentioned_users then self._mentioned_users._array = parseMentions(content, '<@!?(%d+)>') if self._reply_target then insert(self._mentioned_users._array, 1, self._reply_target) end end if self._mentioned_roles then self._mentioned_roles._array = parseMentions(content, '<@&(%d+)>') end if self._mentioned_channels then self._mentioned_channels._array = parseMentions(content, '<#(%d+)>') end if self._mentioned_emojis then self._mentioned_emojis._array = parseMentions(content, '') end self._clean_content = nil end if data.embeds then self._embeds = #data.embeds > 0 and data.embeds or nil end if data.attachments then self._attachments = #data.attachments > 0 and data.attachments or nil end end function Message:_addReaction(d) local reactions = self._reactions if not reactions then reactions = Cache({}, Reaction, self) self._reactions = reactions end local emoji = d.emoji local k = emoji.id ~= null and emoji.id or emoji.name local reaction = reactions:get(k) if reaction then reaction._count = reaction._count + 1 if d.user_id == self.client._user._id then reaction._me = true end else d.me = d.user_id == self.client._user._id d.count = 1 reaction = reactions:_insert(d) end return reaction end function Message:_removeReaction(d) local reactions = self._reactions if not reactions then return nil end local emoji = d.emoji local k = emoji.id ~= null and emoji.id or emoji.name local reaction = reactions:get(k) or nil if not reaction then return nil end -- uncached reaction? reaction._count = reaction._count - 1 if d.user_id == self.client._user._id then reaction._me = false end if reaction._count == 0 then reactions:_delete(k) end return reaction end function Message:_setOldContent(d) local ts = d.edited_timestamp if not ts then return end local old = self._old if old then old[ts] = old[ts] or self._content else self._old = {[ts] = self._content} end end function Message:_modify(payload) local data, err = self.client._api:editMessage(self._parent._id, self._id, payload) if data then self:_setOldContent(data) self:_load(data) return true else return false, err end end --[=[ @m setContent @t http @p content string @r boolean @d Sets the message's content. The message must be authored by the current user (ie: you cannot change the content of messages sent by other users). The content must be from 1 to 2000 characters in length. ]=] function Message:setContent(content) return self:_modify({content = content or null}) end --[=[ @m setEmbed @t http @p embed table @r boolean @d Sets the message's embed. The message must be authored by the current user. (ie: you cannot change the embed of messages sent by other users). ]=] function Message:setEmbed(embed) return self:_modify({embed = embed or null}) end --[=[ @m hideEmbeds @t http @r boolean @d Hides all embeds for this message. ]=] function Message:hideEmbeds() local flags = bor(self._flags or 0, messageFlag.suppressEmbeds) return self:_modify({flags = flags}) end --[=[ @m showEmbeds @t http @r boolean @d Shows all embeds for this message. ]=] function Message:showEmbeds() local flags = band(self._flags or 0, bnot(messageFlag.suppressEmbeds)) return self:_modify({flags = flags}) end --[=[ @m hasFlag @t mem @p flag Message-Flag-Resolvable @r boolean @d Indicates whether the message has a particular flag set. ]=] function Message:hasFlag(flag) flag = Resolver.messageFlag(flag) return band(self._flags or 0, flag) > 0 end --[=[ @m update @t http @p data table @r boolean @d Sets multiple properties of the message at the same time using a table similar to the one supported by `TextChannel.send`, except only `content` and `embed` are valid fields; `mention(s)`, `file(s)`, etc are not supported. The message must be authored by the current user. (ie: you cannot change the embed of messages sent by other users). ]=] function Message:update(data) return self:_modify({ content = data.content or null, embed = data.embed or null, }) end --[=[ @m pin @t http @r boolean @d Pins the message in the channel. ]=] function Message:pin() local data, err = self.client._api:addPinnedChannelMessage(self._parent._id, self._id) if data then self._pinned = true return true else return false, err end end --[=[ @m unpin @t http @r boolean @d Unpins the message in the channel. ]=] function Message:unpin() local data, err = self.client._api:deletePinnedChannelMessage(self._parent._id, self._id) if data then self._pinned = false return true else return false, err end end --[=[ @m addReaction @t http @p emoji Emoji-Resolvable @r boolean @d Adds a reaction to the message. Note that this does not return the new reaction object; wait for the `reactionAdd` event instead. ]=] function Message:addReaction(emoji) emoji = Resolver.emoji(emoji) local data, err = self.client._api:createReaction(self._parent._id, self._id, emoji) if data then return true else return false, err end end --[=[ @m removeReaction @t http @p emoji Emoji-Resolvable @op id User-ID-Resolvable @r boolean @d Removes a reaction from the message. Note that this does not return the old reaction object; wait for the `reactionRemove` event instead. If no user is indicated, then this will remove the current user's reaction. ]=] function Message:removeReaction(emoji, id) emoji = Resolver.emoji(emoji) local data, err if id then id = Resolver.userId(id) data, err = self.client._api:deleteUserReaction(self._parent._id, self._id, emoji, id) else data, err = self.client._api:deleteOwnReaction(self._parent._id, self._id, emoji) end if data then return true else return false, err end end --[=[ @m clearReactions @t http @r boolean @d Removes all reactions from the message. ]=] function Message:clearReactions() local data, err = self.client._api:deleteAllReactions(self._parent._id, self._id) if data then return true else return false, err end end --[=[ @m delete @t http @r boolean @d Permanently deletes the message. This cannot be undone! ]=] function Message:delete() local data, err = self.client._api:deleteMessage(self._parent._id, self._id) if data then local cache = self._parent._messages if cache then cache:_delete(self._id) end return true else return false, err end end --[=[ @m reply @t http @p content string/table @r Message @d Equivalent to `Message.channel:send(content)`. ]=] function Message:reply(content) return self._parent:send(content) end --[=[@p reactions Cache An iterable cache of all reactions that exist for this message.]=] function get.reactions(self) if not self._reactions then self._reactions = Cache({}, Reaction, self) end return self._reactions end --[=[@p mentionedUsers ArrayIterable An iterable array of all users that are mentioned in this message.]=] function get.mentionedUsers(self) if not self._mentioned_users then local users = self.client._users local mentions = parseMentions(self._content, '<@!?(%d+)>') if self._reply_target then insert(mentions, 1, self._reply_target) end self._mentioned_users = ArrayIterable(mentions, function(id) return users:get(id) end) end return self._mentioned_users end --[=[@p mentionedRoles ArrayIterable An iterable array of known roles that are mentioned in this message, excluding the default everyone role. The message must be in a guild text channel and the roles must be cached in that channel's guild for them to appear here.]=] function get.mentionedRoles(self) if not self._mentioned_roles then local client = self.client local mentions = parseMentions(self._content, '<@&(%d+)>') self._mentioned_roles = ArrayIterable(mentions, function(id) local guild = client._role_map[id] return guild and guild._roles:get(id) or nil end) end return self._mentioned_roles end --[=[@p mentionedEmojis ArrayIterable An iterable array of all known emojis that are mentioned in this message. If the client does not have the emoji cached, then it will not appear here.]=] function get.mentionedEmojis(self) if not self._mentioned_emojis then local client = self.client local mentions = parseMentions(self._content, '') self._mentioned_emojis = ArrayIterable(mentions, function(id) local guild = client._emoji_map[id] return guild and guild._emojis:get(id) end) end return self._mentioned_emojis end --[=[@p mentionedChannels ArrayIterable An iterable array of all known channels that are mentioned in this message. If the client does not have the channel cached, then it will not appear here.]=] function get.mentionedChannels(self) if not self._mentioned_channels then local client = self.client local mentions = parseMentions(self._content, '<#(%d+)>') self._mentioned_channels = ArrayIterable(mentions, function(id) local guild = client._channel_map[id] if guild then return guild._text_channels:get(id) or guild._voice_channels:get(id) or guild._categories:get(id) else return client._private_channels:get(id) or client._group_channels:get(id) end end) end return self._mentioned_channels end local usersMeta = {__index = function(_, k) return '@' .. k end} local rolesMeta = {__index = function(_, k) return '@' .. k end} local channelsMeta = {__index = function(_, k) return '#' .. k end} local everyone = '@' .. constants.ZWSP .. 'everyone' local here = '@' .. constants.ZWSP .. 'here' --[=[@p cleanContent string The message content with all recognized mentions replaced by names and with @everyone and @here mentions escaped by a zero-width space (ZWSP).]=] function get.cleanContent(self) if not self._clean_content then local content = self._content local guild = self.guild local users = setmetatable({}, usersMeta) for user in self.mentionedUsers:iter() do local member = guild and guild._members:get(user._id) users[user._id] = '@' .. (member and member._nick or user._username) end local roles = setmetatable({}, rolesMeta) for role in self.mentionedRoles:iter() do roles[role._id] = '@' .. role._name end local channels = setmetatable({}, channelsMeta) for channel in self.mentionedChannels:iter() do channels[channel._id] = '#' .. channel._name end self._clean_content = content :gsub('<@!?(%d+)>', users) :gsub('<@&(%d+)>', roles) :gsub('<#(%d+)>', channels) :gsub('', '%1') :gsub('@everyone', everyone) :gsub('@here', here) end return self._clean_content end --[=[@p mentionsEveryone boolean Whether this message mentions @everyone or @here.]=] function get.mentionsEveryone(self) return self._mention_everyone end --[=[@p pinned boolean Whether this message belongs to its channel's pinned messages.]=] function get.pinned(self) return self._pinned end --[=[@p tts boolean Whether this message is a text-to-speech message.]=] function get.tts(self) return self._tts end --[=[@p nonce string/number/boolean/nil Used by the official Discord client to detect the success of a sent message.]=] function get.nonce(self) return self._nonce end --[=[@p editedTimestamp string/nil The date and time at which the message was most recently edited, represented as an ISO 8601 string plus microseconds when available.]=] function get.editedTimestamp(self) return self._edited_timestamp end --[=[@p oldContent string/table Yields a table containing keys as timestamps and value as content of the message at that time.]=] function get.oldContent(self) return self._old end --[=[@p content string The raw message content. This should be between 0 and 2000 characters in length.]=] function get.content(self) return self._content end --[=[@p author User The object of the user that created the message.]=] function get.author(self) return self._author end --[=[@p channel TextChannel The channel in which this message was sent.]=] function get.channel(self) return self._parent end --[=[@p type number The message type. Use the `messageType` enumeration for a human-readable representation.]=] function get.type(self) return self._type end --[=[@p embed table/nil A raw data table that represents the first rich embed that exists in this message. See the Discord documentation for more information.]=] function get.embed(self) return self._embeds and self._embeds[1] end --[=[@p attachment table/nil A raw data table that represents the first file attachment that exists in this message. See the Discord documentation for more information.]=] function get.attachment(self) return self._attachments and self._attachments[1] end --[=[@p embeds table A raw data table that contains all embeds that exist for this message. If there are none, this table will not be present.]=] function get.embeds(self) return self._embeds end --[=[@p attachments table A raw data table that contains all attachments that exist for this message. If there are none, this table will not be present.]=] function get.attachments(self) return self._attachments end --[=[@p guild Guild/nil The guild in which this message was sent. This will not exist if the message was not sent in a guild text channel. Equivalent to `Message.channel.guild`.]=] function get.guild(self) return self._parent.guild end --[=[@p member Member/nil The member object of the message's author. This will not exist if the message is not sent in a guild text channel or if the member object is not cached. Equivalent to `Message.guild.members:get(Message.author.id)`.]=] function get.member(self) local guild = self.guild return guild and guild._members:get(self._author._id) end --[=[@p referencedMessage Message/nil If available, the previous message that this current message references as seen in replies.]=] function get.referencedMessage(self) return self._referencedMessage end --[=[@p link string URL that can be used to jump-to the message in the Discord client.]=] function get.link(self) local guild = self.guild return format('https://discord.com/channels/%s/%s/%s', guild and guild._id or '@me', self._parent._id, self._id) end --[=[@p webhookId string/nil The ID of the webhook that generated this message, if applicable.]=] function get.webhookId(self) return self._webhook_id end return Message