UNPKG

hubot-slack

Version:
265 lines (233 loc) 10.8 kB
{Message, TextMessage} = require.main.require "hubot" SlackClient = require "./client" SlackMention = require "./mention" Promise = require "bluebird" class ReactionMessage extends Message ###* # Represents a message generated by an emoji reaction event # # @constructor # @param {string} type - A String indicating 'reaction_added' or 'reaction_removed' # @param {User} user - A User instance that reacted to the item. # @param {string} reaction - A String identifying the emoji reaction. # @param {Object} item - An Object identifying the target message, file, or comment item. # @param {User} [item_user] - A String indicating the user that posted the item. If the item was created by a # custom integration (not part of a Slack app with a bot user), then this value will be undefined. # @param {string} event_ts - A String of the reaction event timestamp. ### constructor: (@type, @user, @reaction, @item_user, @item, @event_ts) -> super @user @type = @type.replace("reaction_", "") class FileSharedMessage extends Message ###* # Represents a message generated by an file_shared event # # @constructor # @param {User} user - A User instance that reacted to the item. # @param {string} file_id - A String identifying the file_id of the file that was shared. # @param {string} event_ts - A String of the file_shared event timestamp. ### constructor: (@user, @file_id, @event_ts) -> super @user class PresenceMessage extends Message ###* # Represents a message generated by a presence change event # # @constructor # @param {Array<User>} users - Array of users that changed their status # @param {string} presence - Status is either 'active' or 'away' ### constructor: (@users, @presence) -> # supply the super class with a fake user because the real data is in the `users` property super { room: "" } class SlackTextMessage extends TextMessage @MESSAGE_REGEX = /// < # opening angle bracket ([@#!])? # link type ([^>|]+) # link (?:\| # start of |label (optional) ([^>]+) # label )? # end of label > # closing angle bracket ///g @MESSAGE_RESERVED_KEYWORDS = ["channel","group","everyone","here"] ###* # Represents a TextMessage created from the Slack adapter # # @constructor # @param {User} user - The User who sent this message # @param {string|undefined} text - The parsed message text. Its no longer recommended to use this property. # The `buildText()` method can be used to parse the raw text and populate the `text` property. # @param {string|undefined} rawText - The unparsed message text. Its no longer recommended to use this property. # The constructor will default to the `rawMessage.text` value. # @param {Object} rawMessage - The Slack Message object # @param {string} rawMessage.text # @param {string} rawMessage.ts # @param {string} [rawMessage.thread_ts] - the identifier for the thread the message is a part of # @param {string} [rawMessage.attachments] - Slack message attachments # @param {string} channel_id - The conversation where this message was sent. # @param {string} robot_name - The Slack username for this robot # @param {string} robot_alias - The alias for this robot ### constructor: (@user, @text, rawText, @rawMessage, channel_id, robot_name, robot_alias) -> # private instance properties @_channel_id = channel_id @_robot_name = robot_name @_robot_alias = robot_alias # public instance property initialization @rawText = rawText || @rawMessage.text @thread_ts = @rawMessage.thread_ts if @rawMessage.thread_ts? @mentions = [] super @user, @text, @rawMessage.ts ###* # Build the text property, a flat string representation of the contents of this message. # # @private # @param {SlackClient} client - a client that can be used to get more data needed to build the text # @param {function} cb - callback for the result ### buildText: (client, cb) -> # base text text = if @rawMessage.text? then @rawMessage.text else "" # flatten any attachments into text if @rawMessage.attachments attachment_text = @rawMessage.attachments.map((a) -> a.fallback).join("\n") text = text + "\n" + attachment_text if attachment_text.length > 0 # Replace links in text async to fetch user and channel info (if present) mentionFormatting = @replaceLinks(client, text) # Fetch conversation info fetchingConversationInfo = client.fetchConversation(@_channel_id) Promise.all([mentionFormatting, fetchingConversationInfo]) .then (results) => [ replacedText, conversationInfo ] = results text = replacedText text = text.replace /&lt;/g, "<" text = text.replace /&gt;/g, ">" text = text.replace /&amp;/g, "&" # special handling for message text when inside a DM conversation if conversationInfo.is_im startOfText = if text.indexOf("@") == 0 then 1 else 0 robotIsNamed = text.indexOf(@_robot_name) == startOfText || text.indexOf(@_robot_alias) == startOfText # Assume it was addressed to us even if it wasn't if not robotIsNamed text = "#{@_robot_name} #{text}" # If this is a DM, pretend it was addressed to us @text = text cb() .catch (error) => client.robot.logger.error "An error occurred while building text: #{error.message}" cb(error) ###* # Replace links inside of text # # @private # @param {SlackClient} client - a client that can be used to get more data needed to build the text # @returns {Promise<string>} ### replaceLinks: (client, text) -> regex = SlackTextMessage.MESSAGE_REGEX regex.lastIndex = 0 cursor = 0 parts = [] while (result = regex.exec(text)) [m, type, link, label] = result switch type when "@" if label parts.push(text.slice(cursor, result.index), "@#{label}") @mentions.push new SlackMention(link, "user", undefined) else parts.push(text.slice(cursor, result.index), @replaceUser(client, link, @mentions)) when "#" if label parts.push(text.slice(cursor, result.index), "\##{label}") @mentions.push new SlackMention(link, "conversation", undefined) else parts.push(text.slice(cursor, result.index), @replaceConversation(client, link, @mentions)) when "!" if link in SlackTextMessage.MESSAGE_RESERVED_KEYWORDS parts.push(text.slice(cursor, result.index), "@#{link}") else if label parts.push(text.slice(cursor, result.index), label) else parts.push(text.slice(cursor, result.index), m) else link = link.replace /^mailto:/, "" if label and -1 == link.indexOf label parts.push(text.slice(cursor, result.index), "#{label} (#{link})") else parts.push(text.slice(cursor, result.index), link) cursor = regex.lastIndex if (result[0].length == 0) regex.lastIndex++ parts.push text.slice(cursor) return Promise.all(parts) .then (substrings) -> return substrings.join("") ###* # Creates a mention from a user ID # # @private # @param {SlackClient} client - a client that can be used to get more data needed to build the text # @param {string} id - the user ID # @param {Array<Mention>} mentions - a mentions array that is updated to include this user mention # @returns {Promise<string>} - a string that can be placed into the text for this mention ### replaceUser: (client, id, mentions) -> client.fetchUser(id) .then (res) => mentions.push(new SlackMention(res.id, "user", res)) return "@#{res.name}" .catch (error) => client.robot.logger.error "Error getting user info #{id}: #{error.message}" return "<@#{id}>" ###* # Creates a mention from a conversation ID # # @private # @param {SlackClient} client - a client that can be used to get more data needed to build the text # @param {string} id - the conversation ID # @param {Array<Mention>} mentions - a mentions array that is updated to include this conversation mention # @returns {Promise<string>} - a string that can be placed into the text for this mention ### replaceConversation: (client, id, mentions) -> client.fetchConversation(id) .then (conversation) => if conversation? mentions.push(new SlackMention(conversation.id, "conversation", conversation)) return "\##{conversation.name}" else return "<\##{id}>" .catch (error) => client.robot.logger.error "Error getting conversation info #{id}: #{error.message}" return "<\##{id}>" ###* # Factory method to construct SlackTextMessage # @public # @param {User} user - The User who sent this message # @param {string|undefined} text - The parsed message text. Its no longer recommended to use this property. # The `buildText()` method can be used to parse the raw text and populate the `text` property. # @param {string|undefined} rawText - The unparsed message text. Its no longer recommended to use this property. # The constructor will default to the `rawMessage.text` value. # @param {Object} rawMessage - The Slack Message object # @param {string} rawMessage.text # @param {string} rawMessage.ts # @param {string} [rawMessage.thread_ts] - the identifier for the thread the message is a part of # @param {string} [rawMessage.attachments] - Slack message attachments # @param {string} channel_id - The conversation where this message was sent. # @param {string} robot_name - The Slack username for this robot # @param {string} robot_alias - The alias for this robot # @param {SlackClient} client - client used to fetch more data # @param {function} cb - callback to return the result ### @makeSlackTextMessage: (user, text, rawText, rawMessage, channel_id, robot_name, robot_alias, client, cb) -> message = new SlackTextMessage(user, text, rawText, rawMessage, channel_id, robot_name, robot_alias) # creates a completion function that consistently calls the callback after this function has returned done = (message) -> setImmediate(() -> cb(null, message)) if not message.text? then message.buildText client, (error) -> return cb(error) if error done(message) else done(message) exports.SlackTextMessage = SlackTextMessage exports.ReactionMessage = ReactionMessage exports.PresenceMessage = PresenceMessage exports.FileSharedMessage = FileSharedMessage