hubot-hipchat-dbeard
Version:
A HipChat adapter for Hubot
294 lines (243 loc) • 9.95 kB
text/coffeescript
{Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, User} = require "hubot"
HTTPS = require "https"
{inspect} = require "util"
Connector = require "./connector"
promise = require "./promises"
class HipChat extends Adapter
constructor: (robot) ->
super robot
@logger = robot.logger
reconnectTimer = null
emote: (envelope, strings...) ->
@send envelope, strings.map((str) -> "/me #{str}")...
send: (envelope, strings...) ->
for str in strings
@connector.message envelope.room, str
topic: (envelope, message) ->
{user, room} = envelope
user = envelope if not user # pre-2.4.2 style
target_jid =
# most common case - we're replying to a user in a room or 1-1
user?.reply_to or
# allows user objects to be passed in
user?.jid or
if user?.search?(/@/) >= 0
user # allows user to be a jid string
else
room # this will happen if someone uses robot.messageRoom(jid, ...)
if not target_jid
return @logger.error "ERROR: Not sure who to send to: envelope=#{inspect envelope}"
@connector.topic target_jid, message
reply: (envelope, strings...) ->
user = if envelope.user then envelope.user else envelope
@send envelope, "@#{user.mention_name} #{str}" for str in strings
waitAndReconnect: ->
if !@reconnectTimer
delay = Math.round(Math.random() * (20 - 5) + 5)
@logger.info "Waiting #{delay}s and then retrying..."
@reconnectTimer = setTimeout () =>
@logger.info "Attempting to reconnect..."
delete @reconnectTimer
@connector.connect()
, delay * 1000
run: ->
botjid = process.env.HUBOT_HIPCHAT_JID
if not botjid
throw new Error("Environment variable HUBOT_HIPCHAT_JID is required to contain your bot's user JID.")
botpw = process.env.HUBOT_HIPCHAT_PASSWORD
if not botpw
throw new Error("Environment variable HUBOT_HIPCHAT_PASSWORD is required to contain your bot's user password.")
@options =
jid: botjid
password: botpw
token: process.env.HUBOT_HIPCHAT_TOKEN or null
rooms: process.env.HUBOT_HIPCHAT_ROOMS or "All"
rooms_blacklist: process.env.HUBOT_HIPCHAT_ROOMS_BLACKLIST or ""
rooms_join_public: process.env.HUBOT_HIPCHAT_JOIN_PUBLIC_ROOMS isnt "false"
host: process.env.HUBOT_HIPCHAT_HOST or null
bosh: { url: process.env.HUBOT_HIPCHAT_BOSH_URL or null }
autojoin: process.env.HUBOT_HIPCHAT_JOIN_ROOMS_ON_INVITE isnt "false"
xmppDomain: process.env.HUBOT_HIPCHAT_XMPP_DOMAIN or null
reconnect: process.env.HUBOT_HIPCHAT_RECONNECT isnt "false"
@logger.debug "HipChat adapter options: #{JSON.stringify @options}"
# create Connector object
connector = new Connector
jid: @options.jid
password: @options.password
host: @options.host
logger: @logger
xmppDomain: @options.xmppDomain
bosh: @options.bosh
host = if @options.host then @options.host else "hipchat.com"
@logger.info "Connecting HipChat adapter..."
init = promise()
connector.onTopic (channel, from, message) =>
@logger.info "Topic change: " + message
author = getAuthor: => @robot.brain.userForName(from) or new User(from)
author.room = channel
@receive new TopicMessage(author, message, 'id')
connector.onDisconnect =>
@logger.info "Disconnected from #{host}"
if @options.reconnect
@waitAndReconnect()
connector.onError =>
@logger.error [].slice.call(arguments).map(inspect).join(", ")
if @options.reconnect
@waitAndReconnect()
firstTime = true
connector.onConnect =>
@logger.info "Connected to #{host} as @#{connector.mention_name}"
# Provide our name to Hubot
@robot.name = connector.mention_name
# Tell Hubot we're connected so it can load scripts
if firstTime
@emit "connected"
@logger.debug "Sending connected event"
saveUsers = (users) =>
# Save users to brain
for user in users
user.id = @userIdFromJid user.jid
# userForId will not merge to an existing user
if user.id of @robot.brain.data.users
oldUser = @robot.brain.data.users[user.id]
for key, value of oldUser
unless key of user
user[key] = value
delete @robot.brain.data.users[user.id]
@robot.brain.userForId user.id, user
joinRoom = (jid) =>
if jid and typeof jid is "object"
jid = "#{jid.local}@#{jid.domain}"
if jid in @options.rooms_blacklist.split(",")
@logger.info "Not joining #{jid} because it is blacklisted"
return
@logger.info "Joining #{jid}"
connector.join jid
# Fetch user info
connector.getRoster (err, users, stanza) =>
return init.reject err if err
init.resolve users
init
.done (users) =>
saveUsers(users)
# Join requested rooms
if @options.rooms is "All" or @options.rooms is "@All"
connector.getRooms (err, rooms, stanza) =>
if rooms
for room in rooms
if !@options.rooms_join_public && room.guest_url != ''
@logger.info "Not joining #{room.jid} because it is a public room"
else
joinRoom(room.jid)
else
@logger.error "Can't list rooms: #{errmsg err}"
# Join all rooms
else
for room_jid in @options.rooms.split ","
joinRoom(room_jid)
.fail (err) =>
@logger.error "Can't list users: #{errmsg err}" if err
connector.onRosterChange (users) =>
saveUsers(users)
handleMessage = (opts) =>
# buffer message events until the roster fetch completes
# to ensure user data is properly loaded
init.done =>
{getAuthor, message, room} = opts
author = getAuthor() or {}
author.room = room
@receive new TextMessage(author, message)
if firstTime
connector.onMessage (channel, from, message) =>
# reformat leading @mention name to be like "name: message" which is
# what hubot expects
mention_name = connector.mention_name
regex = new RegExp "^@#{mention_name}\\b", "i"
message = message.replace regex, "#{mention_name}: "
handleMessage
getAuthor: => @robot.brain.userForName(from) or new User(from)
message: message
room: channel
connector.onPrivateMessage (from, message) =>
# remove leading @mention name if present and format the message like
# "name: message" which is what hubot expects
mention_name = connector.mention_name
regex = new RegExp "^@?#{mention_name}\\b", "i"
message = "#{mention_name}: #{message.replace regex, ""}"
handleMessage
getAuthor: => @robot.brain.userForId(@userIdFromJid from)
message: message
room: from
changePresence = (PresenceMessage, user_jid, room_jid, currentName) =>
# buffer presence events until the roster fetch completes
# to ensure user data is properly loaded
init.done =>
user = @robot.brain.userForId(@userIdFromJid(user_jid)) or {}
if user
user.room = room_jid
# If an updated name was sent as part of a presence, update it now
user.name = currentName if currentName.length
@receive new PresenceMessage(user)
if firstTime
connector.onEnter (user_jid, room_jid, currentName) =>
changePresence EnterMessage, user_jid, room_jid, currentName
connector.onLeave (user_jid, room_jid) ->
changePresence LeaveMessage, user_jid, room_jid
connector.onInvite (room_jid, from_jid, message) =>
action = if @options.autojoin then "joining" else "ignoring"
@logger.info "Got invite to #{room_jid} from #{from_jid} - #{action}"
joinRoom(room_jid) if @options.autojoin
firstTime = false
connector.connect()
@connector = connector
userIdFromJid: (jid) ->
try
jid.match(/^\d+_(\d+)@chat\./)[1]
catch e
@logger.error "Bad user JID: #{jid}"
# Convenience HTTP Methods for posting on behalf of the token'd user
get: (path, callback) ->
@request "GET", path, null, callback
post: (path, body, callback) ->
@request "POST", path, body, callback
request: (method, path, body, callback) ->
@logger.debug "Request:", method, path, body
host = @options.host or "api.hipchat.com"
headers = "Host": host
unless @options.token
return callback "No API token provided to Hubot", null
options =
agent : false
host : host
port : 443
path : path += "?auth_token=#{@options.token}"
method : method
headers: headers
if method is "POST"
headers["Content-Type"] = "application/x-www-form-urlencoded"
options.headers["Content-Length"] = body.length
request = HTTPS.request options, (response) =>
data = ""
response.on "data", (chunk) ->
data += chunk
response.on "end", =>
if response.statusCode >= 400
@logger.error "HipChat API error: #{response.statusCode}"
try
callback null, JSON.parse(data)
catch err
callback null, data or { }
response.on "error", (err) ->
callback err, null
if method is "POST"
request.end(body, "binary")
else
request.end()
request.on "error", (err) =>
@logger.error err
@logger.error err.stack if err.stack
callback err
errmsg = (err) ->
err + (if err.stack then '\n' + err.stack else '')
exports.use = (robot) ->
new HipChat robot