Skip to content

Commit

Permalink
Dumping almost everything and starting over. Have the basics of conne…
Browse files Browse the repository at this point in the history
…cting and receiving messages working
  • Loading branch information
grantmd authored and paulhammond committed Dec 8, 2014
1 parent f181a19 commit c14b4d1
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 234 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,3 @@ This adapter uses the following environment variables:
#### HUBOT\_SLACK\_TOKEN

This is the API token for the Slack user you would like to run Hubot under.

#### HUBOT\_SLACK\_BOTNAME

Optional. What your Hubot is called on Slack. If you entered `slack-hubot` here, you would address your bot like `slack-hubot: help`. Otherwise, defaults to `slackbot`.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,14 @@
"hubot": "~2.6.4",
"grunt-contrib-watch": "~0.5.3",
"grunt-shell": "~0.5.0",
"node-slack": "~0.9.0"
"slack-client": "~1.0.0"
},
"main": "./src/slack",
"engines": {
"node": ">=0.4.7"
},
"scripts": {
"test": "mocha --compilers coffee:coffee-script/register --reporter spec"
}
},
"dependencies": {}
}
284 changes: 56 additions & 228 deletions src/slack.coffee
Original file line number Diff line number Diff line change
@@ -1,257 +1,85 @@
{Robot, Adapter, TextMessage} = require 'hubot'
https = require 'https'
SlackClient = require 'slack-client'
Util = require 'util'

class Slack extends Adapter
class SlackBot extends Adapter
constructor: (robot) ->
super robot
@channelMapping = {}


###################################################################
# Slightly abstract logging, primarily so that it can
# be easily altered for unit tests.
###################################################################
log: console.log.bind console
logError: console.error.bind console


###################################################################
# Communicating back to the chat rooms. These are exposed
# as methods on the argument passed to callbacks from
# robot.respond, robot.listen, etc.
###################################################################
send: (envelope, strings...) ->
@log "Sending message"
channel = envelope.reply_to || @channelMapping[envelope.room] || envelope.room

strings.forEach (str) =>
str = @escapeHtml str
args = JSON.stringify
username : @robot.name
channel : channel
text : str
link_names : @options.link_names if @options?.link_names?

@post "/services/hooks/hubot", args

reply: (envelope, strings...) ->
@log "Sending reply"

user_name = envelope.user?.name || envelope?.name

strings.forEach (str) =>
@send envelope, "#{user_name}: #{str}"

topic: (params, strings...) ->
# TODO: Set the topic


custom: (message, data)->
@log "Sending custom message"
channel = message.reply_to || @channelMapping[message.room] || message.room
data = [data] unless Array.isArray data
attachments = []
for item in data
attachments.push
text : @escapeHtml item.text
fallback : @escapeHtml item.fallback
pretext : @escapeHtml item.pretext
color : item.color
fields : item.fields
mrkdwn_in : item.mrkdwn_in
args = JSON.stringify
username : message.username || @robot.name
icon_url : message.icon_url
icon_emoji : message.icon_emoji
channel : channel
attachments : attachments
link_names : @options.link_names if @options?.link_names?
@post "/services/hooks/hubot", args
###################################################################
# HTML helpers.
###################################################################
escapeHtml: (string) ->
try
string
# Escape entities
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')

# Linkify. We assume that the bot is well-behaved and
# consistently sending links with the protocol part
.replace(/((\bhttp)\S+)/g, '<$1>')
catch e
@logError "Failed to escape HTML: #{e}"
return ''

unescapeHtml: (string) ->
try
string
# Unescape entities
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')

# Convert markup into plain url string.
.replace(/<((\bhttps?)[^|]+)(\|(.*))+>/g, '$1')
.replace(/<((\bhttps?)(.*))?>/g, '$1')
catch e
@logError "Failed to unescape HTML: #{e}"
return ''


###################################################################
# Parsing inputs.
###################################################################

parseOptions: ->
@options =
token : process.env.HUBOT_SLACK_TOKEN
team : process.env.HUBOT_SLACK_TEAM
name : process.env.HUBOT_SLACK_BOTNAME or 'slackbot'
mode : process.env.HUBOT_SLACK_CHANNELMODE or 'blacklist'
# Make sure channel settings don't include leading hashes
channels: (process.env.HUBOT_SLACK_CHANNELS?.split(',') or []).map (channel) ->
channel.replace /^#/, ''
link_names: process.env.HUBOT_SLACK_LINK_NAMES or 0

getMessageFromRequest: (req) ->
# Check the token
return if not req.param('token') or (req.param('token') isnt @options.token)

# Parse the payload
hubotMsg = req.param 'text'
room = req.param 'channel_name'
mode = @options.mode
channels = @options.channels

@unescapeHtml hubotMsg if hubotMsg and (mode is 'blacklist' and room not in channels or mode is 'whitelist' and room in channels)

getAuthorFromRequest: (req) ->
# Return an author object
id : req.param 'user_id'
name : req.param 'user_name'

userFromParams: (params) ->
# hubot < 2.4.2: params = user
# hubot >= 2.4.2: params = {user: user, ...}
user = {}
if params.user
user = params.user
else
user = params

if user.room and not user.reply_to
user.reply_to = user.room

user
###################################################################
# The star.
###################################################################
run: ->
self = @
@parseOptions()
# Take our options from the environment, and set otherwise suitable defaults
options =
token: process.env.HUBOT_SLACK_TOKEN
autoReconnect: true
autoMark: true

@log "Slack adapter options:", @options
@robot.logger.info Util.inspect(options)
return @robot.logger.error "No services token provided to Hubot" unless options.token

return @logError "No services token provided to Hubot" unless @options.token
return @logError "No team provided to Hubot" unless @options.team
@options = options

@robot.on 'slack-attachment', (payload)=>
@custom(payload.message, payload.content)
# Create our slack client object
@client = new SlackClient options.token, options.autoReconnect, options.autoMark

# Listen to incoming webhooks from slack
self.robot.router.post "/hubot/slack-webhook", (req, res) ->
self.log "Incoming message received"
# Setup event handlers
# TODO: I think hubot would like to know when people come online
# TODO: Handle eventual events at (re-)connection time for unreads and provide a config for whether we want to process them
@client.on 'error', @.error
@client.on 'loggedIn', @.loggedIn
@client.on 'open', @.open
@client.on 'close', @.close
@client.on 'message', @.message

hubotMsg = self.getMessageFromRequest req
author = self.getAuthorFromRequest req
author = self.robot.brain.userForId author.id, author
author.reply_to = req.param 'channel_id'
author.room = req.param 'channel_name'
self.channelMapping[req.param 'channel_name'] = req.param 'channel_id'
# Start logging in
@client.login()

if hubotMsg and author
# Pass to the robot
self.receive new TextMessage(author, hubotMsg)
error: (error) =>
@robot.logger.error "Received error #{error.toString()}"

# Just send back an empty reply, since our actual reply,
# if any, will be async above
res.end ""
loggedIn: (self, team) =>
@robot.logger.info "Logged in as #{self.name} of #{team.name}, but not yet connected"

# Provide our name to Hubot
self.robot.name = @options.name
@robot.name = self.name

# Tell Hubot we're connected so it can load scripts
@log "Successfully 'connected' as", self.robot.name
self.emit "connected"


###################################################################
# Convenience HTTP Methods for sending data back to slack.
###################################################################
get: (path, callback) ->
@request "GET", path, null, callback

post: (path, body, callback) ->
@request "POST", path, body, callback
open: =>
@robot.logger.info 'Slack client connected'

request: (method, path, body, callback) ->
self = @

host = "#{@options.team}.slack.com"
headers =
Host: host
# Tell Hubot we're connected so it can load scripts
@emit "connected"

path += "?token=#{@options.token}"
close: =>
@robot.logger.info 'Slack client closed'

reqOptions =
agent : false
hostname : host
port : 443
path : path
method : method
headers : headers
message: (message) =>
if message.hidden then return
if not message.text and not message.attachments then return

if method is "POST"
body = new Buffer body
reqOptions.headers["Content-Type"] = "application/x-www-form-urlencoded"
reqOptions.headers["Content-Length"] = body.length
channel = @client.getChannelGroupOrDMByID message.channel
user = @client.getUserByID message.user
# TODO: Handle message.username for bot messages?
# TODO: Do we need to ignore our own messages? Probably!

request = https.request reqOptions, (response) ->
data = ""
response.on "data", (chunk) ->
data += chunk
# Build message text to respond to, including all attachments
txt = ''
if message.text then txt += message.text

if message.attachments
for k, attach of message.attachments
if k > 0 then txt += "\n"
txt += attach.fallback

response.on "end", ->
if response.statusCode >= 400
self.logError "Slack services error: #{response.statusCode}"
self.logError data
# TODO: Need to process the user into a full hubot user using @robot.brain.userForId user etc

#console.log "HTTPS response:", data
callback? null, data
@robot.logger.debug "Received message: #{txt} in channel: #{channel.name}, from: #{user.name}"

response.on "error", (err) ->
self.logError "HTTPS response error:", err
callback? err, null
@receive new TextMessage(user, txt)

if method is "POST"
request.end body, "binary"
else
request.end()
# TODO: Send

request.on "error", (err) ->
self.logError "HTTPS request error:", err
self.logError err.stack
callback? err
# TODO: Reply

# TODO: Topic

###################################################################
# Exports to handle actual usage and unit testing.
###################################################################
exports.use = (robot) ->
new Slack robot

# Export class for unit tests
exports.Slack = Slack
new SlackBot robot

0 comments on commit c14b4d1

Please sign in to comment.