Skip to content

Commit

Permalink
Merge pull request slackapi#133 from kballard/bot-messages
Browse files Browse the repository at this point in the history
Create new Message types for slack messages
  • Loading branch information
paulhammond committed Jan 6, 2015
2 parents 3c915db + 1c49e4d commit c4b1b91
Show file tree
Hide file tree
Showing 8 changed files with 415 additions and 95 deletions.
15 changes: 15 additions & 0 deletions index.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
SlackBot = require './src/slack'
{SlackTextMessage, SlackRawMessage, SlackBotMessage} = require './src/message'
{SlackRawListener, SlackBotListener} = require './src/listener'

module.exports = exports = {
SlackBot
SlackTextMessage
SlackRawMessage
SlackBotMessage
SlackRawListener
SlackBotListener
}

exports.use = (robot) ->
new SlackBot robot
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
"mocha": "~1.13.0",
"grunt-contrib-watch": "~0.5.3",
"grunt-shell": "~0.5.0",
"hubot": "~2.10"
"hubot": "~2.11"
},
"main": "./src/slack",
"main": "./index",
"engines": {
"node": ">=0.4.7"
},
Expand Down
46 changes: 46 additions & 0 deletions src/listener.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{Listener} = require 'hubot'
{SlackRawMessage, SlackBotMessage} = require './message'

class SlackRawListener extends Listener
# SlackRawListeners receive SlackRawMessages from the Slack adapter
# and decide if they want to act on it.
#
# robot - A Robot instance.
# matcher - A Function that determines if this listener should trigger the
# callback. The matcher takes a SlackRawMessage.
# callback - A Function that is triggered if the incoming message matches.
#
# To use this listener in your own script, you can say
#
# robot.listeners.push new SlackRawListener(robot, matcher, callback)
constructor: (@robot, @matcher, @callback) ->

# Public: Invokes super only for instances of SlackRawMessage
call: (message) ->
if message instanceof SlackRawMessage
super message
else
false

class SlackBotListener extends Listener
# SlackBotListeners receive SlackBotMessages from the Slack adapter
# and decide if they want to act on it. SlackBotListener will only
# match instances of SlackBotMessage.
#
# robot - A Robot instance.
# regex - A Regex that determines if this listener should trigger the
# callback.
# callback - A Function that is triggered if the incoming message matches.
#
# To use this listener in your own script, you can say
#
# robot.listeners.push new SlackBotListener(robot, regex, callback)
constructor: (@robot, @regex, @callback) ->
@matcher = (message) =>
if message instanceof SlackBotMessage
message.match @regex

module.exports = {
SlackRawListener
SlackBotListener
}
56 changes: 56 additions & 0 deletions src/message.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{Message, TextMessage} = require 'hubot'

# Hubot only started exporting Message in 2.11.0. Previous version do not export
# this class. In order to remain compatible with older versions, we can pull the
# Message class from TextMessage superclass.
if not Message
Message = TextMessage.__super__.constructor

class SlackTextMessage extends TextMessage
# Represents a TextMessage created from the Slack adapter
#
# user - The User object
# text - The parsed message text
# rawText - The unparsed message text
# rawMessage - The Slack Message object
constructor: (@user, @text, @rawText, @rawMessage) ->
super @user, @text, @rawMessage.ts

class SlackRawMessage extends Message
# Represents Slack messages that are not suitable to treat as text messages.
# These are hidden messages, or messages that have no text / attachments.
#
# Note that the `user` property may be a "fake" user, i.e. one that does not
# exist in Hubot's brain and that contains little to no data.
#
# user - The User object
# text - The parsed message text, if any, or ""
# rawText - The unparsed message text, if any, or ""
# rawMessage - The Slack Message object
constructor: (@user, @text = "", @rawText = "", @rawMessage) ->
super @user

class SlackBotMessage extends SlackRawMessage
# Represents a message sent by a bot. Specifically, this is any message
# with the subtype "bot_message". Expect the `user` property to be a
# "fake" user.

# Determines if the message matches the given regex.
#
# regex - A Regex to check.
#
# Returns a Match object or null.
match: (regex) ->
@text.match regex

# String representation of a SlackBotMessage
#
# Returns the message text
toString: () ->
@text

module.exports = {
SlackTextMessage
SlackRawMessage
SlackBotMessage
}
55 changes: 33 additions & 22 deletions src/slack.coffee
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{Robot, Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage} = require 'hubot'
{Robot, Adapter, EnterMessage, LeaveMessage, TopicMessage} = require 'hubot'
{SlackTextMessage, SlackRawMessage, SlackBotMessage} = require './message'
{SlackRawListener, SlackBotListener} = require './listener'

SlackClient = require 'slack-client'
Util = require 'util'
Expand Down Expand Up @@ -89,19 +91,33 @@ class SlackBot extends Adapter
process.exit 0

message: (msg) =>
return if msg.hidden
return if not msg.text and not msg.attachments

# Ignore bot messages (TODO: make this support an option?)
return if msg.subtype is 'bot_message'

# Ignore message subtypes that don't have a top level user property
return if not msg.user

# Ignore our own messages
return if msg.user == @self.id

channel = @client.getChannelGroupOrDMByID msg.channel
channel = @client.getChannelGroupOrDMByID msg.channel if msg.channel

if msg.hidden or (not msg.text and not msg.attachments) or msg.subtype is 'bot_message' or not msg.user or not channel
# use a raw message, so scripts that care can still see these things

if msg.user
user = @robot.brain.userForId msg.user
else
# We need to fake a user because, at the very least, CatchAllMessage
# expects it to be there.
user = {}
user.name = msg.username if msg.username?
user.room = channel.name if channel

rawText = msg.getBody()
text = @removeFormatting rawText

if msg.subtype is 'bot_message'
@robot.logger.debug "Received bot message: '#{text}' in channel: #{channel?.name}, from: #{user?.name}"
@receive new SlackBotMessage user, text, rawText, msg
else
@robot.logger.debug "Received raw message (subtype: #{msg.subtype})"
@receive new SlackRawMessage user, text, rawText, msg
return

# Process the user into a full hubot user
user = @robot.brain.userForId msg.user
Expand All @@ -122,17 +138,16 @@ class SlackBot extends Adapter

else
# Build message text to respond to, including all attachments
txt = msg.getBody()
rawText = msg.getBody()
text = @removeFormatting rawText

txt = @removeFormatting txt

@robot.logger.debug "Received message: '#{txt}' in channel: #{channel.name}, from: #{user.name}"
@robot.logger.debug "Received message: '#{text}' in channel: #{channel.name}, from: #{user.name}"

# If this is a DM, pretend it was addressed to us
if msg.getChannelType() == 'DM'
txt = "#{@robot.name} #{txt}"
text = "#{@robot.name} #{text}"

@receive new TextMessage user, txt, msg.ts
@receive new SlackTextMessage user, text, rawText, msg

removeFormatting: (text) ->
# https://api.slack.com/docs/formatting
Expand Down Expand Up @@ -227,8 +242,4 @@ class SlackBot extends Adapter
channel = @client.getChannelGroupOrDMByName envelope.room
channel.setTopic strings.join "\n"

exports.use = (robot) ->
new SlackBot robot

# Export class for unit tests
exports.SlackBot = SlackBot
module.exports = SlackBot
169 changes: 169 additions & 0 deletions test/message.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
{SlackTextMessage, SlackRawMessage, SlackBotMessage, SlackRawListener, SlackBotListener} = require '../index'

should = require 'should'

class ClientMessage
ts = 0
constructor: (fields = {}) ->
@type = 'message'
@ts = (ts++).toString()
@[k] = val for own k, val of fields

getBody: ->
# match what slack-client.Message does
text = ""
text += @text if @text
if @attachments
text += "\n" if @text
for k, attach of @attachments
text += "\n" if k > 0
text += attach.fallback
text

getChannelType: ->
# we only simulate channels for now
'Channel'

describe 'Receiving a Slack message', ->
beforeEach ->
@makeMessage = (fields = {}) =>
msg = new ClientMessage fields
msg.channel = @stubs.channel.id unless 'channel' of fields
msg

it 'should produce a SlackTextMessage', ->
@slackbot.message @makeMessage {
user: @stubs.user.id
text: "Hello world"
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackTextMessage
msg.text.should.equal "Hello world"

it 'should parse the text in the SlackTextMessage', ->
@slackbot.message @makeMessage {
user: @stubs.user.id
text: "Foo <@U123> bar <http://slack.com>"
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackTextMessage
msg.text.should.equal "Foo @name bar http://slack.com"
msg.rawText.should.equal "Foo <@U123> bar <http://slack.com>"

it 'should include attachments in the SlackTextMessage text', ->
@slackbot.message @makeMessage {
user: @stubs.user.id
text: "Hello world"
attachments: [
{ fallback: "attachment fallback" }
{ fallback: "second attachment fallback" }
]
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackTextMessage
msg.text.should.equal "Hello world\nattachment fallback\nsecond attachment fallback"

it 'should save the raw message in the SlackTextMessage', ->
@slackbot.message rawMsg = @makeMessage {
subtype: 'file_share'
user: @stubs.user.id
text: "Hello world"
file:
name: "file.txt"
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackTextMessage
msg.rawMessage.should.equal rawMsg

it 'should produce a SlackBotMessage when the subtype is bot_message', ->
@slackbot.message rawMsg = @makeMessage {
subtype: 'bot_message'
username: 'bot'
text: 'Hello world'
attachments: [{
fallback: 'attachment'
}]
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackBotMessage
msg.text.should.equal "Hello world\nattachment"
msg.rawMessage.should.equal rawMsg

it 'should produce a SlackRawMessage when the user is nil', ->
@slackbot.message rawMsg = @makeMessage {
text: 'Hello world'
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackRawMessage
msg.text.should.equal 'Hello world'
msg.rawMessage.should.equal rawMsg

it 'should produce a SlackRawMessage when the message is hidden', ->
@slackbot.message @makeMessage {
hidden: true
user: @stubs.user.id
text: 'Hello world'
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackRawMessage

it 'should produce a SlackRawMessage when the message has no body', ->
@slackbot.message @makeMessage {
user: @stubs.user.id
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackRawMessage

it 'should produce a SlackRawMessage when the message has no channel', ->
@slackbot.message new ClientMessage {
type: 'message'
user: @stubs.user.id
text: 'Hello world'
ts: "1234"
}
@stubs.robot.received.should.have.length 1
msg = @stubs.robot.received[0]
msg.should.be.an.instanceOf SlackRawMessage

describe 'should handle SlackRawMessage inheritance properly when Hubot', ->
# this is a bit of a wacky one
# We need to muck with the require() machinery
# To that end, we're going to save off the modules we need to tweak, so we can
# remove the cache and reload from disk
beforeEach ->
mods = (require.resolve name for name in ['hubot', '../src/message'])
@saved = []
# ensure the modules are loaded
require path for path in mods # ensure the modules are loaded
@saved = (require.cache[path] for path in mods) # grab the modules
delete require.cache[path] for path in mods # remove the modules from the require cache

afterEach ->
# restore the saved modules
for mod in @saved
require.cache[mod.filename] = mod
delete @saved

it 'does not export Message', ->
delete require('hubot').Message # remove hubot.Message if it exists
{SlackRawMessage: rawMessage} = require '../src/message'
rawMessage::constructor.__super__.constructor.name.should.equal 'Message'

it 'does export Message', ->
if not require('hubot').Message
# create a placeholder class to use here
# We're not actually running any code from Message during the evaluation
# of src/message.coffee so we don't need the real class.
# note: using JavaScript escape because CoffeeScript doesn't allow shadowing otherwise
`function Message() {}`
require('hubot').Message = Message
{SlackRawMessage: rawMessage} = require '../src/message'
rawMessage::constructor.__super__.constructor.name.should.equal 'Message'
Loading

0 comments on commit c4b1b91

Please sign in to comment.