Table of Contents
-
Hubot Scripting
- Hello, World!
- Basic Operations
- Reacting To Messages In Chatroom
- Reacting To Message Parts
- Capturing All Messages
- Capturing Unhandled Messages
- Serving HTTP Requests
- Cross Script Communication With Events
- Periodic Task Execution
- Debugging Your Scripts
- Advanced Debugging With Node Inspector
- Writing Unit Tests For Hubot Scripts
- Hubot Script Template
- Using Hubot Shell Adapter For Script Development
- Developing Scripts With Hubot Control
- Learning More
Hubot Scripting
Hubot is written in Node.js, using CoffeeScript,
which is a JavaScript wrapper that resembles Python and aims to remove the shortcomings of
JavaScript and make use of it’s wonderful object model. Following the tradition of Hubot, all
scripts in this book will be written in CoffeeScript, but you may use JavaScript, simply name your
scripts with .js
rather than .coffee
file extension.
Hello, World!
To start with, create scripts/hello.coffee
in your hubot directory with following contents:
# Description:
# Greet the world
#
# Commands:
# hubot greet - Say hello to the world
module.exports =
(robot) ->
robot
.
respond
/greet/i
,
(msg) ->
msg
.
send
"Hello, World!"
Now restart Hubot and try it out in your chatroom.
Tomas V. hubot help greet
Hubot hubot greet - Say hello to the world
Tomas V. hubot greet
Hubot Hello, World!
Wonderful, isn’t it?
Basic Operations
Hubot is event driven, and when you write scripts for it, you define callbacks that should happen when some event occurs. Event can be:
- Message in the chatroom
- Private message to Hubot
- A text pattern detected in any message
- HTTP request
Callback can result in:
- Message in the chatroom
- Reply to a message
- Emotion in the chatroom
- HTTP response (if trigger was HTTP request)
- New HTTP request
- Executing a shell command
- Executing something on a remote server
Hubot can do anything that can be done with Node.js.
We’ll learn how to exploit everything Hubot can offer by writing a fully functional script that covers a different piece of functionality. We will be analyzing it line by line, so you will get a perfectly clear understanding of what’s happening.
Reacting To Messages In Chatroom
Let’s try to create something more useful than hello world. We want Hubot to print out this month’s
calendar when we say “hubot calendar” or “hubot calendar me”. We will use cal
- a shell command
that prints out a calendar like this:
hubot@botserv:~$
cal
January 2014
Su Mo Tu We Th Fr Sa
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
To do that, we will create calendar.coffee
in scripts/
directory and use robot.respond
to
handle the event.
scripts/calendar.coffee
1
child_process =
require
(
'child_process'
)
2
module.exports =
(robot) ->
3
robot
.
respond
/calendar( me)?/i
,
(msg) ->
4
child_process
.
exec
'cal -h'
,
(error, stdout, stderr) ->
5
msg
.
send
(
stdout
)
Let’s analyze what happens line by line.
1
child_process =
require
(
'child_process'
)
Here we require
child_process - a node
module for making system calls. We assign the module to child_process
variable.
2
module.exports =
(robot) ->
When Hubot requires calendar.coffee
, module.exports
is the object that gets returned. The
(robot) ->
part is a function that takes robot
argument. This is how this line would look like
in JavaScript:
module
.
exports
=
function
(
robot
)
{
Every Hubot script must export a function that takes robot
argument and uses it to set up event
listeners.
3
robot
.
respond
/calendar( me)?/i
,
(msg) ->
robot.respond
is a function that takes two arguments - a regular expression to match the message,
and a callback function that takes msg
argument, which has a variety of functions for doing
various actions. The regex /calendar( me)?/i
would match calendar
and calendar me
in case
insensitive fashion. Since we are using respond
, it also expects the message to begin with
hubot
, or whatever your bot name is.
4
child_process
.
exec
'cal -h'
,
(error, stdout, stderr) ->
Here we call exec
function on child_process
variable, and provide two parameters - a system
call that should be executed, and a callback function that takes 3 arguments - error
, stdout
,
and stderr
. cal -h
displays ASCII calendar without highlighting current day.
5
msg
.
send
(
stdout
)
Finally, we use msg
, which was passed into robot.respond
callback function, to send standard
output from cal -h
command that we just executed.
To understand Hubot scripting better, you can try to understand concepts of Node.js. It’s all about
callbacks. In our calendar script there are two nested callbacks, one for robot.respond
, another
for child_process.exec
.
Now restart Hubot and test the new script.
Tomas V. hubot calendar
Hubot January 2014
Su Mo Tu We Th Fr Sa
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
It works as expected, but we also want this command to appear in hubot help
, since it’s not
useful to have commands that nobody knows about. We have to add a documentation block on
top of our script to get the effect. The final version of our script looks like this:
script/calendar.coffee
# Description:
# Prints out this month's ASCII calendar.
#
# Commands:
# hubot calendar [me] - Print out this month's calendar
child_process =
require
(
'child_process'
)
module.exports =
(robot) ->
robot
.
respond
/calendar( me)?/i
,
(msg) ->
child_process
.
exec
'cal -h'
,
(error, stdout, stderr) ->
msg
.
send
(
stdout
)
Now hubot help
and hubot help calendar
will tell everyone about your script.
Reacting To Message Parts
Hubot can eavesdrop on chatrooms and react to certain words or phrases that were said without
talking to the bot directly. Use robot.hear
to do it.
Our new script will listen for “weather in <…>”, query Open Weather Map API and post the weather information.
script/weather.coffee
1
# Description:
2
# Tells the weather
3
#
4
# Configuration:
5
# HUBOT_WEATHER_API_URL - Optional openweathermap.org API endpoint to use
6
# HUBOT_WEATHER_UNITS - Temperature units to use. 'metric' or 'imperial'
7
#
8
# Commands:
9
# weather in <location> - Tells about the weather in given location
10
#
11
# Author:
12
# spajus
13
14
process
.
env
.
HUBOT_WEATHER_API_URL
||=
15
'http://api.openweathermap.org/data/2.5/weather'
16
process
.
env
.
HUBOT_WEATHER_UNITS
||=
'imperial'
17
18
module.exports =
(robot) ->
19
robot
.
hear
/weather in (\w+)/i
,
(msg) ->
20
city =
msg
.
match
[
1
]
21
query =
{
units:
process
.
env
.
HUBOT_WEATHER_UNITS
,
q:
city
}
22
url =
process
.
env
.
HUBOT_WEATHER_API_URL
23
msg
.
robot
.
http
(
url
).
query
(
query
).
get
()
(err, res, body) ->
24
data =
JSON
.
parse
(
body
)
25
weather =
[
"
#{
Math
.
round
(
data
.
main
.
temp
)
}
degrees"
]
26
for
w
in
data
.
weather
27
weather
.
push
w
.
description
28
msg
.
reply
"It's
#{
weather
.
join
(
', '
)
}
in
#{
data
.
name
}
,
#{
data
.
sys
.
count
\
29
ry
}
"
Run it for a test drive.
Tomas V. I wonder what is the weather in Vilnius right now
Hubot Tomas Varaneckas: It's 28 degrees, shower snow, mist in Vilnius, LT
Tomas V. and weather in California?
Hubot Tomas Varaneckas: It's 37 degrees, Sky is Clear in California, US
I wish I were in California right now. Anyway, let’s take this script apart. We’ll skip documentation, since it’s pretty straightforward.
14
process
.
env
.
HUBOT_WEATHER_API_URL
||=
15
'http://api.openweathermap.org/data/2.5/weather'
16
process
.
env
.
HUBOT_WEATHER_UNITS
||=
'imperial'
process.env
allows you to read and set environmental variables, and our script uses a couple
of them. One for defining the API endpoint, another one for measurment unit type. In CoffeeScript
x ||= y
is a shorthand for x = (x != null) ? x : y
, meaning it will only set the variable if
it has not been set before. This way you can override the values and set
HUBOT_WEATHER_UNITS=metric
to get Hubot tell degrees in Celsius rather than Farenheit.
19
robot
.
hear
/weather in (\w+)/i
,
(msg) ->
robot.hear
works almost like robot.respond
, with one exception. robot.respond
requires
message to begin with Hubot’s name, while robot.hear
reacts on any part of message, which is
exactly what we want. It takes two arguments, a regex that matches “weather in <location>", and a
callback function which will be triggered if a match is found.</location>
20
city =
msg
.
match
[
1
]
msg.match
is an array of regex matches, with 0 being the full message, and in our case 1 being
the content of the parentheses, which is simply any word. Yes, this script will fail to work with
“San Francisco”. So, we set city to be the first word that comes after “weather in”.
21
query =
{
units:
process
.
env
.
HUBOT_WEATHER_UNITS
,
q:
city
}
22
url =
process
.
env
.
HUBOT_WEATHER_API_URL
Here we construct a query string parameters that will be passed to the weather API, and set the URL
we are going to call. We will read units
from HUBOT_WEATHER_UNITS
environmental variable, and set
query to city
. If we would construct the query string ourselves, we would need to worry about
URL-encoding special characters, but since we’re passing an object, it will be taken care of for
us. Final request will be made to following url:
http://api.openweathermap.org/data/2.5/weather?units=imperial&q=chicago
.
23
msg
.
robot
.
http
(
url
).
query
(
query
).
get
()
(err, res, body) ->
24
data =
JSON
.
parse
(
body
)
Now we call the url using HTTP GET
, set the query string parametrs using .query()
, and provide
a callback function to handle the response. Callback parameters are error (if any), HTTP response
object and plain text response body. Our API returns JSON, so we parse the response body into
data
variable.
25
weather =
[
"
#{
Math
.
round
(
data
.
main
.
temp
)
}
degrees"
]
Here we create a weather
array with single element - data.main.temp
is { main: { temp: ... }
}
from the response JSON, and since it is returned in high precision, we round it to an integer
with Math.round
. And finally we make it a string with “degrees” at the end.
26
for
w
in
data
.
weather
27
weather
.
push
w
.
description
We loop { weather: [ ... ] }
from response JSON, getting the description
out of every element
and pushing it to the end of weather
array.
28
msg
.
reply
"It's
#{
weather
.
join
(
', '
)
}
in
#{
data
.
name
}
,
#{
data
.
sys
.
count
\
29
ry
}
"
When we have our weather
array all packed up with data, we join it into comma separated string
and form a nice string containing the weather data, city name and country code.
Capturing All Messages
Sometimes you may want Hubot to process all messages in all chatrooms. For example, if you are writing a logging system. Here is a simple one:
scripts/logger.coffee
1
# Description
2
# Logs all conversations
3
#
4
# Notes:
5
# Logs can be found at bot's logs/ directory
6
#
7
# Author:
8
# spajus
9
10
module.exports =
(robot) ->
11
fs =
require
'fs'
12
fs
.
exists
'./logs/'
,
(exists) ->
13
if
exists
14
startLogging
()
15
else
16
fs
.
mkdir
'./logs/'
,
(error) ->
17
unless
error
18
startLogging
()
19
else
20
console
.
log
"Could not create logs directory:
#{
error
}
"
21
startLogging =
->
22
console
.
log
"Started logging"
23
robot
.
hear
//
,
(msg) ->
24
fs
.
appendFile
logFileName
(
msg
),
formatMessage
(
msg
),
(error) ->
25
console
.
log
"Could not log message:
#{
error
}
"
if
error
26
logFileName =
(msg) ->
27
safe_room_name =
"
#{
msg
.
message
.
room
}
"
.
replace
/[^a-z0-9]/ig
,
''
28
"./logs/
#{
safe_room_name
}
.log"
29
formatMessage =
(msg) ->
30
"[
#{
new
Date
()
}
]
#{
msg
.
message
.
user
.
name
}
:
#{
msg
.
message
.
text
}
\n"
The breakdown:
11
fs =
require
'fs'
We require Node’s built in file system module and assign it to
fs
variable.
12
fs
.
exists
'./logs/'
,
(exists) ->
We check if ./logs/
directory exists,
and since NodeJS is asynchronous, we have to provide a callback function (exists) ->
, that will
get called with true
or false
after file system check actually happens.
13
if
exists
14
startLogging
()
15
else
16
fs
.
mkdir
'./logs/'
,
(error) ->
17
unless
error
18
startLogging
()
19
else
20
console
.
log
"Could not create logs directory:
#{
error
}
"
All this is happening in the (exists) ->
callback function. If directory ./logs/
exists, we
start logging by calling startLogging()
function immediately, otherwise we call
mkdir to create this directory.
It has another callback function, (error) ->
. It gets called after directory creation is over. If
there was no error, we call startLogging()
function, otherwise we use console.log
to inform
that we failed to start logging because directory could not be created.
21
startLogging =
->
22
console
.
log
"Started logging"
23
robot
.
hear
//
,
(msg) ->
This is the definition of startLogging()
function we’ve called above. It uses console.log
to
announce that logging was initiated, then uses robot.hear //, (msg) ->
to
register a listener that reacts to all chat messages. That is because robot.hear
does not require
a message to be prefixed with hubot
, and //
is a regular expression that would match just
anything.
24
fs
.
appendFile
logFileName
(
msg
),
formatMessage
(
msg
),
(error) ->
25
console
.
log
"Could not log message:
#{
error
}
"
if
error
When robot.hear
gets triggered, (msg) ->
is called, and this is what happens inside. We use
appendFile to
create or append a file that logFileName(msg)
function will return, and write the output of
formatMessage(msg)
function there. appendFile
has a callback function to handle errors. We
define it as (error) ->
and use console.log
to inform about the failure if error
is present.
Time to try this out. After restarting Hubot, say something:
Tomas V. Hello, anybody here?
hubot ping
Hubot PONG
Tomas V. oh good, I hope you're not logging anything
It should appear in your Hubot’s logs/
directory:
hubot@botserv: ~/campfire$
cat logs/585164.log
[2014-03-22 21:54:26] Tomas Varaneckas: Hello, anybody here?
[2014-03-22 21:54:32] Tomas Varaneckas: hubot ping
[2014-03-22 21:54:47] Tomas Varaneckas: oh good, I hope you're not logging an\
ything
Unfortunately Hubot will not be able to see it’s own messages. It can be done after tweaking the internals, but that’s a whole different story. Other than that, all messages will get logged.
Capturing Unhandled Messages
If you want to capture only those messages that were not handled by any Hubot script, it’s very simple to do:
1
module.exports =
(robot) ->
2
robot
.
catchAll
(msg) ->
3
msg
.
send
"I don't know how to react to:
#{
msg
.
message
.
text
}
"
Serving HTTP Requests
Hubot has a built-in express web framework that can serve HTTP requests. By
default it runs on port 8080
, but you can change the value using PORT
environmental variable.
This time we will create a script that responds to HTTP requests and posts request body in one or
more rooms. We’ll name this script notifier.coffee
.
It will accept HTTP POST requests, so there will be no limits for what the body can be.
scripts/notifier.coffee
1
# Description:
2
# Send message to chatroom using HTTP POST
3
#
4
# URLS:
5
# POST /hubot/notify/<room> (message=<message>)
6
7
module.exports =
(robot) ->
8
robot
.
router
.
post
'/hubot/notify/:room'
,
(req, res) ->
9
room =
req
.
params
.
room
10
message =
req
.
body
.
message
11
robot
.
messageRoom
room
,
message
12
res
.
end
()
To try it out, we will make a POST
request using curl
.
hubot@botserv:~$
curl -X POST \
-d message="Hello from $(hostname) shell" \
http://localhost:8080/hubot/notify/585164
And we get this in our chatroom.
Hubot Hello from botserv shell
Let’s dig in to the source.
8
robot
.
router
.
post
'/hubot/notify/:room'
,
(req, res) ->
robot.router.post
creates a listener for HTTP POST requests to /hubot/notify/:room
URL, where
:room
is a variable defining your room. It also takes a callback function that has two
parameters, request and response. You can find out everything about robot.router
by examiming
express api documentation - robot.router
is the express app
.
9
room =
req
.
params
.
room
req.params
contains params from the URL, so in our case, if URL is /hubot/notify/123
, variable
room
is set to 123
.
10
message =
req
.
body
.
message
We read the value of message
POST parameter and assign it to message
variable.
11
robot
.
messageRoom
room
,
message
12
res
.
end
()
Now, we send the message
to given room
and end the HTTP response. It would work without
res.end()
, but it’s always nice to respond to the request, otherwise the HTTP client may hang
while expecting a response.
While this script looks nothing important, this concept is incredibly useful in building your own chat based monitoring. You can trigger any sort of events from anywhere and make Hubot tell everything about it by doing an HTTP request.
Cross Script Communication With Events
To reduce script complexity, or to introduce communication between two or more scripts, one can use
Hubot event system, which consists of two simple functions: robot.emit event, args
and robot.on
event, (args) ->
. We will now write two scrips - event-master.coffee
and event-slave.coffee
.
Master will listen to us and trigger events that Slave will listen to and process.
scripts/event-master.coffee
1
# Description:
2
# Controls slave at event-slave.coffee
3
#
4
# Commands:
5
# hubot tell slave to <action> - Emits event to slave to do the action
6
7
module.exports =
(robot) ->
8
robot
.
respond
/tell slave to (.*)/i
,
(msg) ->
9
action =
msg
.
match
[
1
]
10
room =
msg
.
message
.
room
11
msg
.
send
"Master: telling slave to
#{
action
}
"
12
robot
.
emit
'slave:command'
,
action
,
room
scripts/event-slave.coffee
1
# Description:
2
# Executes commands from `event-master.coffee`
3
4
module.exports =
(robot) ->
5
robot
.
on
'slave:command'
,
(action, room) ->
6
robot
.
messageRoom
room
,
"Slave: doing as told:
#{
action
}
"
7
console
.
log
'Screw you, master...'
It runs like this:
Tomas V. hubot tell slave to bring beer
Hubot Slave: doing as told: bring beer
Hubot Master: telling slave to bring beer
Meanwhile in hubot.log
:
hubot.log
Screw you, master...
Notice that “Slave” responded before “Master”, even though msg.send
was called in “Master”
script first. It’s a perfect example to help you understand how Node.js works. Nearly everything is
being done asynchronously using callback functions, the only way to ensure the order of execution
is to use callbacks. To make “Master” send his message first, we have to put robot.emit
in
msg.send
callback in event-master.coffee
, like this:
11
msg
.
send
"Master: telling slave to
#{
action
}
"
,
->
12
robot
.
emit
'slave:command'
,
action
,
room
This way msg.send
is execute first, and only when it’s done, the callback function is called and
robot.emit
gets executed.
In robot.emit
call, slave:command
is just a string that describes the event, action
and
room
are the parameters that are passed along with the event trigger. There can be as many
listeners as needed for every event type. We have placed ours in event-slave.coffee
:
5
robot
.
on
'slave:command'
,
(action, room) ->
6
robot
.
messageRoom
room
,
"Slave: doing as told:
#{
action
}
"
7
console
.
log
'Screw you, master...'
Our callback function is pretty simple, it just posts a message to given room, and logs “Screw
you, master…” behind everyone’s back using console.log
. It’s a good technique for debugging
your scripts.
Periodic Task Execution
You can make Hubot execute something using node-cron, which works perfectly with combination of firing events - let one of your scripts listen to an event, and another one fire them periodically.
First install the dependencies in your Hubot directory:
hubot@botserv:~campfire$
npm install --save cron time
Then create a script called scripts/cron.coffee
and define all periodic executions there:
scripts/cron.coffee
1
# Description:
2
# Defines periodic executions
3
4
module.exports =
(robot) ->
5
cronJob =
require
(
'cron'
).
CronJob
6
tz =
'America/Los_Angeles'
7
new
cronJob
(
'0 0 9 * * 1-5'
,
workdaysNineAm
,
null
,
true
,
tz
)
8
new
cronJob
(
'0 */5 * * * *'
,
everyFiveMinutes
,
null
,
true
,
tz
)
9
10
room =
12345678
11
12
workdaysNineAm =
->
13
robot
.
emit
'slave:command'
,
'wake everyone up'
,
room
14
15
everyFiveMinutes =
->
16
robot
.
messageRoom
room
,
'I will nag you every 5 minutes'
Now let’s break it down:
5
cronJob =
require
(
'cron'
).
CronJob
6
tz =
'America/Los_Angeles'
7
new
cronJob
(
'0 0 9 * * 1-5'
,
workdaysNineAm
,
null
,
true
,
tz
)
8
new
cronJob
(
'0 */5 * * * *'
,
everyFiveMinutes
,
null
,
true
,
tz
)
Here we require the cron
dependency and assign it’s CronJob
prototype to cronJob
variable
and assign our desired time zone to tz
.
Then we create two jobs, first will run every workday at 9 AM in Los Angeles time and will execute
workdaysNineAm
function. The other one will execute every five minutes and call
everyFiveMinutes
function.
10
room =
12345678
11
12
workdaysNineAm =
->
13
robot
.
emit
'slave:command'
,
'wake everyone up'
,
room
14
15
everyFiveMinutes =
->
16
robot
.
messageRoom
room
,
'I will nag you every 5 minutes'
We assign room id to room
variable, which we will use in following functions. workdaysNineAm
emits an event for the slave script we created earlier, and everyFiveMinutes
just posts a message
to a room.
You can also do the same automation using your OS cron that would run curl
on Hubot’s HTTP
endpoints, but this is more elegant.
Debugging Your Scripts
It’s frustrating when things don’t work the way they should, but there are several techniques to help you narrow down the problem.
Log strings to hubot.log
:
console
.
log
"Something happened:
#{
this
}
and
#{
that
}
"
Inspect an object and print it in the chatroom:
util =
require
(
'util'
)
msg
.
send
util
.
inspect
(
strange_object
)
Recover from an error and log it:
try
dangerous
.
actions
()
catch
e
console
.
log
"My script failed"
,
e
Advanced Debugging With Node Inspector
Sometimes it’s not enough just to print out the errors. For those occasions you may need heavy
artillery - a full fledged debugger. Luckily, there is
node-inspector. You will be especially happy
with it if you are familiar with Chrome’s web inspector. To use node-inspector
, first install the
npm
package. You should do it once, using -g
switch to install it globally. Install as root.
root@botserv:~#
npm install -g node-inspector
To start the debugger, run node-inspector
either in the background (followed by &
) or
in a new shell. In following example it’s started without preloading all scripts (otherwise it’s a
long wait), and inspector console running on port 8123, because both hubot
and node-inspector
use port 8080
by default. We could set PORT=8123
for hubot
instead, but setting it for
node-inspector
is more convenient.
hubot@focus:~/campfire$
node-inspector --no-preload --web-port 8123
Node Inspector v0.7.0-1
info - socket.io started
Visit http://127.0.0.1:8123/debug?port=5858 to start debugging.
Now, we will put debugger
to add a breakpoint to our weather.coffee
script and debugger will
stop on that line when it gets executed.
script/weather.coffee
27
for
w
in
data
.
weather
28
weather
.
push
w
.
description
29
debugger
30
msg
.
reply
"It's
#{
weather
.
join
(
', '
)
}
in
#{
data
.
name
}
,
#{
data
.
sys
.
count
\
31
ry
}
"
Now we have to start Hubot in a little different way:
hubot@focus:~/campfire$
coffee --nodejs --debug node_modules/.bin/hubot
debugger listening on port 5858
Then open http://127.0.0.1:8123/debug?port=5858
- the link that node-inspector
gave you in it’s
output in Chrome, or any other Blink based browser. Expect a little delay, because it will load
all the scripts that Hubot normally requires just in time when needed. When you are able to see
Sources tree in the top-left corner of your browser (you may need to click on the icon to expand
it), get back to Hubot console and ask for the weather:
Hubot> what is the weather in Hawaii?
Hubot>
Don’t expect a response, because Chrome should now switch to weather.coffee
and stop the
execution at debugger
line. Now you can step over the script line by line, add additional
breakpoints by clicking on line nubers in any souce file from the Source tree in the left, or use
the interactive console - there is Console tab at the top of the debugger, and a small >
icon in
bottom-left corner, which I prefer because it doesn’t close the source view.
You can type any JavaScript in the console, and it will execute. Let’s examine our weather
array:
>
weather
-
Array
[
2
]
0
:
"74 degrees"
1
:
"broken clouds"
length
:
2
And the response from the weather API:
>
data
-
Object
base
:
"cmc stations"
+
clouds
:
Object
cod
:
200
+
coord
:
Object
dt
:
1389847230
id
:
5856195
+
main
:
Object
name
:
""
-
sys
:
Object
country
:
"United States of America"
message
:
0.308
sunrise
:
1389892287
sunset
:
1389931892
-
weather
:
Array
[
1
]
-
0
:
Object
description
:
"broken clouds"
icon
:
"04n"
id
:
803
main
:
"Clouds"
length
:
1
+
wind
:
Object
You can expand any part of the object tree to see what’s in it. You can also call functions:
>
msg
.
send
(
"Hello from node-inspector"
)
And in Hubot shell you should see:
Hubot> Hello from node-inspector
You can debug your web applications or any other JavaScript or CoffeeScript code using this technique. It’s even easier for web applications - just open Chrome Inspector and you’re set.
Writing Unit Tests For Hubot Scripts
Unit tests for Hubot scripts are a tricky subject that is either misunderstood or avoided. There is a strange trend among packages in github.com/hubot-scripts to write tests like this one:
chai =
require
'chai'
sinon =
require
'sinon'
chai
.
use
require
'sinon-chai'
expect =
chai
.
expect
describe
'hangouts'
,
->
beforeEach
->
@robot =
respond:
sinon
.
spy
()
hear:
sinon
.
spy
()
require
(
'../src/hangouts'
)(
@robot
)
it
'registers a respond listener'
,
->
expect
(
@robot
.
respond
).
to
.
have
.
been
.
calledWith
(
/hangout/
)
You can find this test at hubot-scripts/hubot-google-hangouts. This test checks that Hubot script compiles and that it has the following lines:
module.exports =
(robot) ->
robot
.
respond
/hangout( me)?\s*(.+)?/
,
(msg) ->
That’s better than nothing, but still a bit pointless, don’t you think? Luckily, there are better ways to do this. Take a look at hubot-mock-adapter. Tests will certainly be more difficult to write, but they would actually test the script itself, not just the fact that it gets loaded.
To see an example of hubot-mock-adapter
in action, take a look at
tests of hubot-pubsub.
Since unit testing is a vast subject and it can take another book to fully cover, we’re not going to dig any deeper.
Hubot Script Template
You can use this template as a starting point for your new Hubot scripts. It is taken from Hubot Control, which also gives you a web based IDE for quick scripting.
scripts/template.coffee
# Description
# <description of the scripts functionality>
#
# Dependencies:
# "<module name>": "<module version>"
#
# Configuration:
# LIST_OF_ENV_VARS_TO_SET
#
# Commands:
# hubot <trigger> - <what the respond trigger does>
# <trigger> - <what the hear trigger does>
#
# URLS:
# GET /path?param=<val> - <what the request does>
#
# Notes:
# <optional notes required for the script>
#
# Author:
# <github username of the original script author>
module.exports =
(robot) ->
robot
.
respond
/jump/i
,
(msg) ->
msg
.
emote
"jumping!"
robot
.
hear
/your'e/i
,
(msg) ->
msg
.
send
"you're"
robot
.
hear
/what year is it\?/i
,
(msg) ->
msg
.
reply
new
Date
().
getFullYear
()
robot
.
router
.
get
"/foo"
,
(req, res) ->
res
.
end
"bar"
This is how these examples look in action:
Tomas V. hubot jump
Hubot *jumping!*
Tomas V. wow, your'e amazing
Hubot you're
Tomas V. anybody knows what year is it?
Hubot Tomas Varaneckas: 2014
To check HTTP response, we’ll use curl
:
hubot@botserv:~$
curl http://localhost:8080/foo
bar
Using Hubot Shell Adapter For Script Development
You may find it inconvenient to restart Hubot every time you change your script. In many cases you can test your work using built-in shell adapter, like this:
hubot@botserv:~/campfire$
PORT
=
8888 bin/hubot
[Fri Jan 10 2014 01:35:37 GMT-0500 (EST)] INFO Data for brain retrieved from \
Redis
Hubot> hubot help greet
Hubot> Hubot greet - Say hello to the world
Hubot> hubot greet
Hubot> Hello, World!
Hubot> exit
In this example we set PORT=8888
to avoid “Address already in use” error if Hubot is alread
running as a service.
Developing Scripts With Hubot Control
If you use Hubot Control, you can develop scripts with it’s web based editor, which offers
syntax checking and highlighting, integration with git
, and a way to restart Hubot without
logging in to the server.
Learning More
We’ve scratched the surface of what you can do with Hubot. One of the best ways to learn more about writing Hubot scripts by studying the source code of existing ones. Best places to start:
- The old script catalog: https://github.com/github/hubot-scripts
- The new script packages: https://github.com/hubot-scripts
Throughout the rest of the book we will cover a number of use cases of integrating Hubot with a variety of applications and web services. You will learn how to make Hubot an invaluable addition to your DevOps stack.