hubot-slack
Version:
A Slack adapter for hubot
265 lines (233 loc) • 10.8 kB
text/coffeescript
{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 /</g, "<"
text = text.replace />/g, ">"
text = text.replace /&/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