Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

438 lines (376 sloc) 14.467 kB
Fs = require 'fs'
Log = require 'log'
Path = require 'path'
HttpClient = require 'scoped-http-client'
User = require './user'
Brain = require './brain'
Response = require './response'
{Listener,TextListener} = require './listener'
{TextMessage,EnterMessage,LeaveMessage,CatchAllMessage} = require './message'
inspect = require('util').inspect
HUBOT_DEFAULT_ADAPTERS = [ 'campfire', 'shell' ]
HUBOT_DOCUMENTATION_SECTIONS = [ 'description', 'dependencies', 'configuration', 'commands', 'notes', 'author', 'examples', 'urls' ]
class Robot
# Robots receive messages from a chat source (Campfire, irc, etc), and
# dispatch them to matching listeners.
#
# adapterPath - A String of the path to local adapters.
# adapter - A String of the adapter name.
# httpd - A Boolean whether to enable the HTTP daemon.
# name - A String of the robot name, defaults to Hubot.
constructor: (adapterPath, adapter, httpd, name = 'Hubot') ->
@name = name
@brain = new Brain
@alias = false
@adapter = null
@Response = Response
@commands = []
@listeners = []
@loadPaths = []
@enableSlash = false
@logger = new Log process.env.HUBOT_LOG_LEVEL or 'info'
@parseVersion()
@setupConnect() if httpd
@loadAdapter adapterPath, adapter if adapter?
@documentation = {}
# Public: Specify a router and callback to register as Connect middleware.
#
# route - A String of the route to match.
# callback - A Function that is called when the route is requested.
#
# Returns nothing.
route: (route, callback) ->
@router.get route, callback
# Public: Adds a Listener that attempts to match incoming messages based on
# a Regex.
#
# regex - A Regex that determines if the callback should be called.
# callback - A Function that is called with a Response object.
#
# Returns nothing.
hear: (regex, callback) ->
@listeners.push new TextListener(@, regex, callback)
# Public: Adds a Listener that attempts to match incoming messages directed
# at the robot based on a Regex. All regexes treat patterns like they begin
# with a '^'
#
# regex - A Regex that determines if the callback should be called.
# callback - A Function that is called with a Response object.
#
# Returns nothing.
respond: (regex, callback) ->
re = regex.toString().split('/')
re.shift() # remove empty first item
modifiers = re.pop() # pop off modifiers
if re[0] and re[0][0] is '^'
@logger.warning "Anchors don't work well with respond, perhaps you want to use 'hear'"
@logger.warning "The regex in question was #{regex.toString()}"
pattern = re.join('/') # combine the pattern back again
if @alias
alias = @alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') # escape alias for regexp
newRegex = new RegExp("^(?:#{alias}[:,]?|#{@name}[:,]?)\\s*(?:#{pattern})", modifiers)
else
newRegex = new RegExp("^#{@name}[:,]?\\s*(?:#{pattern})", modifiers)
@logger.debug newRegex.toString()
@listeners.push new TextListener(@, newRegex, callback)
# Public: Adds a Listener that triggers when anyone enters the room.
#
# callback - A Function that is called with a Response object.
#
# Returns nothing.
enter: (callback) ->
@listeners.push new Listener(@, ((msg) -> msg instanceof EnterMessage), callback)
# Public: Adds a Listener that triggers when anyone leaves the room.
#
# callback - A Function that is called with a Response object.
#
# Returns nothing.
leave: (callback) ->
@listeners.push new Listener(@, ((msg) -> msg instanceof LeaveMessage), callback)
# Public: Adds a Listener that triggers when no other text matchers match.
#
# callback - A Function that is called with a Response object.
#
# Returns nothing.
catchAll: (callback) ->
@listeners.push new Listener(@, ((msg) -> msg instanceof CatchAllMessage), ((msg) -> msg.message = msg.message.message; callback msg))
# Public: Passes the given message to any interested Listeners.
#
# message - A Message instance. Listeners can flag this message as 'done' to
# prevent further execution.
#
# Returns nothing.
receive: (message) ->
results = []
for listener in @listeners
try
results.push listener.call(message)
break if message.done
catch error
@logger.error "Unable to call the listener: #{error}\n#{error.stack}"
false
if message not instanceof CatchAllMessage and results.indexOf(true) is -1
@receive new CatchAllMessage(message)
# Public: Loads every script in the given path.
#
# path - A String path on the filesystem.
#
# Returns nothing.
load: (path) ->
@logger.debug "Loading scripts from #{path}"
Fs.exists path, (exists) =>
if exists
@loadPaths.push path
for file in Fs.readdirSync(path)
@loadFile path, file
# Public: Loads a file in path.
#
# path - A String path on the filesystem.
# file - A String filename in path on the filesystem.
#
# Returns nothing.
loadFile: (path, file) ->
ext = Path.extname file
full = Path.join path, Path.basename(file, ext)
if ext is '.coffee' or ext is '.js'
try
require(full) @
@parseHelp "#{path}/#{file}"
catch error
@logger.error "Unable to load #{full}: #{error}\n#{error.stack}"
# Public: Load scripts specfic in the `hubot-scripts.json` file.
#
# path - A String path to the hubot-scripts files.
# scripts - An Array of scripts to load.
#
# Returns nothing.
loadHubotScripts: (path, scripts) ->
@logger.debug "Loading hubot-scripts from #{path}"
for script in scripts
@loadFile path, script
# Setup the Connect server's defaults.
#
# Returns nothing.
setupConnect: ->
user = process.env.CONNECT_USER
pass = process.env.CONNECT_PASSWORD
Connect = require 'connect'
Connect.router = require 'connect_router'
@connect = Connect()
@connect.use Connect.basicAuth(user, pass) if user and pass
@connect.use Connect.bodyParser()
@connect.use Connect.router (app) =>
@router =
get: (route, callback) =>
@logger.debug "Registered route: GET #{route}"
app.get route, callback
post: (route, callback) =>
@logger.debug "Registered route: POST #{route}"
app.post route, callback
put: (route, callback) =>
@logger.debug "Registered route: PUT #{route}"
app.put route, callback
delete: (route, callback) =>
@logger.debug "Registered route: DELETE #{route}"
app.delete route, callback
@server = @connect.listen process.env.PORT || 8080
herokuUrl = process.env.HEROKU_URL
if herokuUrl
herokuUrl += '/' unless /\/$/.test herokuUrl
setInterval =>
HttpClient.create("#{herokuUrl}hubot/ping")
.post() (err, res, body) =>
@logger.info 'keep alive ping!'
, 1200000
# Load the adapter Hubot is going to use.
#
# path - A String of the path to adapter if local.
# adapter - A String of the adapter name to use.
#
# Returns nothing.
loadAdapter: (path, adapter) ->
@logger.debug "Loading adapter #{adapter}"
try
path = if adapter in HUBOT_DEFAULT_ADAPTERS
"#{path}/#{adapter}"
else
"hubot-#{adapter}"
@adapter = require("#{path}").use(@)
catch err
@logger.error "Cannot load adapter #{adapter} - #{err}\n#{err.stack}"
# Public: Help Commands for Running Scripts.
#
# Returns an Array of help commands for running scripts.
helpCommands: ->
@commands.sort()
# Private: load help info from a loaded script.
#
# path - A String path to the file on disk.
#
# Returns nothing.
parseHelp: (path) ->
@logger.debug "parseHelp of #{path}"
scriptName = Path.basename(path).replace /\.(coffee|js)$/, ''
scriptDocumentation = {}
@documentation[scriptName] = scriptDocumentation
@logger.debug "parseHelp populating @documentation[#{scriptName}]"
Fs.readFile path, 'utf-8', (err, body) =>
throw err if err?
currentSection = null
for line in body.split "\n"
break unless line[0] is '#' or line.substr(0, 2) is '//'
# remove leading comment
cleanedLine = line.replace(/^(#|\/\/)\s?/, "").trim()
@logger.debug "parseHelp(#{scriptName}): read #{cleanedLine}"
continue if cleanedLine.length is 0
continue if cleanedLine.toLowerCase() is 'none'
nextSection = cleanedLine.toLowerCase().replace(':', '')
if nextSection in HUBOT_DOCUMENTATION_SECTIONS
currentSection = nextSection
scriptDocumentation[currentSection] = []
@logger.debug "parseHelp(#{scriptName}): adding #{currentSection} section"
# lines in a section _do_ have leading whitespace
else
if currentSection
@logger.debug "parseHelp(#{scriptName}) adding '#{cleanedLine.trim()}' to #{currentSection}"
scriptDocumentation[currentSection].push cleanedLine.trim()
if currentSection == 'commands'
@commands.push cleanedLine.trim()
# no current section? probably using old style documentation
if currentSection is null
@logger.info "#{path} is using deprecated documentation syntax"
scriptDocumentation.commands = []
for line in body.split("\n")
break if not (line[0] == '#' or line.substr(0, 2) == '//')
continue if not line.match('-')
cleanedLine = line[2..line.length].replace(/^hubot/i, @name).trim()
@logger.debug "parseHelp(#{scriptName}) adding '#{cleanedLine}' to commands"
scriptDocumentation.commands.push cleanedLine
@commands.push cleanedLine
# Public: A helper send function which delegates to the adapter's send
# function.
#
# user - A User instance.
# strings - One or more Strings for each message to send.
#
# Returns nothing.
send: (user, strings...) ->
@adapter.send user, strings...
# Public: A helper send function to message a room that the robot is in.
#
# room - String designating the room to message.
# strings - One or more Strings for each message to send.
#
# Returns nothing.
messageRoom: (room, strings...) ->
user = { room: room }
@adapter.send user, strings...
# Public: A helper reply function which delegates to the adapter's reply
# function.
#
# user - A User instance.
# strings - One or more Strings for each message to send.
#
# Returns nothing.
reply: (user, strings...) ->
@adapter.reply user, strings...
# Public: Get an Array of User objects stored in the brain.
#
# Returns an Array of User objects.
users: ->
@brain.data.users
# Public: Get a User object given a unique identifier.
#
# Returns a User instance of the specified user.
userForId: (id, options) ->
user = @brain.data.users[id]
unless user
user = new User id, options
@brain.data.users[id] = user
if options and options.room and (!user.room or user.room isnt options.room)
user = new User id, options
@brain.data.users[id] = user
user
# Public: Get a User object given a name.
#
# Returns a User instance for the user with the specified name.
userForName: (name) ->
result = null
lowerName = name.toLowerCase()
for k of (@brain.data.users or { })
userName = @brain.data.users[k]['name']
if userName? and userName.toLowerCase() is lowerName
result = @brain.data.users[k]
result
# Public: Get all users whose names match fuzzyName. Currently, match
# means 'starts with', but this could be extended to match initials,
# nicknames, etc.
#
# Returns an Array of User instances matching the fuzzy name.
usersForRawFuzzyName: (fuzzyName) ->
lowerFuzzyName = fuzzyName.toLowerCase()
user for key, user of (@brain.data.users or {}) when (
user.name.toLowerCase().lastIndexOf(lowerFuzzyName, 0) == 0)
# Public: If fuzzyName is an exact match for a user, returns an array with
# just that user. Otherwise, returns an array of all users for which
# fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).
#
# Returns an Array of User instances matching the fuzzy name.
usersForFuzzyName: (fuzzyName) ->
matchedUsers = @usersForRawFuzzyName(fuzzyName)
lowerFuzzyName = fuzzyName.toLowerCase()
# We can scan matchedUsers rather than all users since usersForRawFuzzyName
# will include exact matches
for user in matchedUsers
return [user] if user.name.toLowerCase() is lowerFuzzyName
matchedUsers
# Public: Kick off the event loop for the adapter
#
# Returns nothing.
run: ->
@adapter.run()
# Public: Gracefully shutdown the robot process
#
# Returns nothing.
shutdown: ->
@adapter.close()
@brain.close()
# Public: The version of Hubot from npm
#
# Returns a String of the version number.
parseVersion: ->
package_path = __dirname + '/../package.json'
data = Fs.readFileSync package_path, 'utf8'
content = JSON.parse data
@version = content.version
# Public: Creates a scoped http client with chainable methods for
# modifying the request. This doesn't actually make a request though.
# Once your request is assembled, you can call `get()`/`post()`/etc to
# send the request.
#
# url - String URL to access.
#
# Examples:
#
# res.http("http://example.com")
# # set a single header
# .header('Authorization', 'bearer abcdef')
#
# # set multiple headers
# .headers(Authorization: 'bearer abcdef', Accept: 'application/json')
#
# # add URI query parameters
# .query(a: 1, b: 'foo & bar')
#
# # make the actual request
# .get() (err, res, body) ->
# console.log body
#
# # or, you can POST data
# .post(data) (err, res, body) ->
# console.log body
#
# Returns a ScopedClient instance.
http: (url) ->
HttpClient.create(url)
module.exports = Robot
Jump to Line
Something went wrong with that request. Please try again.