UNPKG

42.6 kBMarkdownView Raw
1---
2permalink: /docs/scripting/
3---
4
5# Scripting
6
7Hubot out of the box doesn't do too much but it is an extensible, scriptable robot friend. There are [hundreds of scripts written and maintained by the community](index.md#scripts) and it's easy to write your own. You can create a custom script in hubot's `scripts` directory or [create a script package](#creating-a-script-package) for sharing with the community!
8
9## Anatomy of a script
10
11When you created your hubot, the generator also created a `scripts` directory. If you peek around there, you will see some examples of scripts. For a script to be a script, it needs to:
12
13* live in a directory on the hubot script load path (`src/scripts` and `scripts` by default)
14* be a `.coffee` or `.js` file
15* export a function
16
17By export a function, we just mean:
18
19```coffeescript
20module.exports = (robot) ->
21 # your code here
22```
23
24The `robot` parameter is an instance of your robot friend. At this point, we can start scripting up some awesomeness.
25
26## Hearing and responding
27
28Since this is a chat bot, the most common interactions are based on messages. Hubot can `hear` messages said in a room or `respond` to messages directly addressed at it. Both methods take a regular expression and a callback function as parameters. For example:
29
30```coffeescript
31module.exports = (robot) ->
32 robot.hear /badger/i, (res) ->
33 # your code here
34
35 robot.respond /open the pod bay doors/i, (res) ->
36 # your code here
37```
38
39The `robot.hear /badger/` callback is called anytime a message's text matches. For example:
40
41* Stop badgering the witness
42* badger me
43* what exactly is a badger anyways
44
45The `robot.respond /open the pod bay doors/i` callback is only called for messages that are immediately preceded by the robot's name or alias. If the robot's name is HAL and alias is /, then this callback would be triggered for:
46
47* hal open the pod bay doors
48* HAL: open the pod bay doors
49* @HAL open the pod bay doors
50* /open the pod bay doors
51
52It wouldn't be called for:
53
54* HAL: please open the pod bay doors
55 * because its `respond` is bound to the text immediately following the robot name
56* has anyone ever mentioned how lovely you are when you open the pod bay doors?
57 * because it lacks the robot's name
58
59## Send & reply
60
61The `res` parameter is an instance of `Response` (historically, this parameter was `msg` and you may see other scripts use it this way). With it, you can `send` a message back to the room the `res` came from, `emote` a message to a room (If the given adapter supports it), or `reply` to the person that sent the message. For example:
62
63```coffeescript
64module.exports = (robot) ->
65 robot.hear /badger/i, (res) ->
66 res.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"
67
68 robot.respond /open the pod bay doors/i, (res) ->
69 res.reply "I'm afraid I can't let you do that."
70
71 robot.hear /I like pie/i, (res) ->
72 res.emote "makes a freshly baked pie"
73```
74
75The `robot.hear /badgers/` callback sends a message exactly as specified regardless of who said it, "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS".
76
77If a user Dave says "HAL: open the pod bay doors", `robot.respond /open the pod bay doors/i` callback sends a message "Dave: I'm afraid I can't let you do that."
78
79## Messages to a room or user
80
81Messages can be sent to a specified room or user using the messageRoom function.
82
83```coffeescript
84module.exports = (robot) ->
85
86 robot.hear /green eggs/i, (res) ->
87 room = "mytestroom"
88 robot.messageRoom room, "I do not like green eggs and ham. I do not like them sam-I-am."
89```
90
91User name can be explicitely specified if desired ( for a cc to an admin/manager), or using
92the response object a private message can be sent to the original sender.
93
94```coffeescript
95 robot.respond /I don't like Sam-I-am/i, (res) ->
96 room = 'joemanager'
97 robot.messageRoom room, "Someone does not like Dr. Seus"
98 res.reply "That Sam-I-am\nThat Sam-I-am\nI do not like\nthat Sam-I-am"
99
100 robot.hear /Sam-I-am/i, (res) ->
101 room = res.envelope.user.name
102 robot.messageRoom room, "That Sam-I-am\nThat Sam-I-am\nI do not like\nthat Sam-I-am"
103```
104
105## Capturing data
106
107So far, our scripts have had static responses, which while amusing, are boring functionality-wise. `res.match` has the result of `match`ing the incoming message against the regular expression. This is just a [JavaScript thing](http://www.w3schools.com/jsref/jsref_match.asp), which ends up being an array with index 0 being the full text matching the expression. If you include capture groups, those will be populated `res.match`. For example, if we update a script like:
108
109```coffeescript
110 robot.respond /open the (.*) doors/i, (res) ->
111 # your code here
112```
113
114If Dave says "HAL: open the pod bay doors", then `res.match[0]` is "open the pod bay doors", and `res.match[1]` is just "pod bay". Now we can start doing more dynamic things:
115
116```coffeescript
117 robot.respond /open the (.*) doors/i, (res) ->
118 doorType = res.match[1]
119 if doorType is "pod bay"
120 res.reply "I'm afraid I can't let you do that."
121 else
122 res.reply "Opening #{doorType} doors"
123```
124
125## Making HTTP calls
126
127Hubot can make HTTP calls on your behalf to integrate & consume third party APIs. This can be through an instance of [node-scoped-http-client](https://github.com/technoweenie/node-scoped-http-client) available at `robot.http`. The simplest case looks like:
128
129
130```coffeescript
131 robot.http("https://midnight-train")
132 .get() (err, response, body) ->
133 # your code here
134```
135
136A post looks like:
137
138```coffeescript
139 data = JSON.stringify({
140 foo: 'bar'
141 })
142 robot.http("https://midnight-train")
143 .header('Content-Type', 'application/json')
144 .post(data) (err, response, body) ->
145 # your code here
146```
147
148
149`err` is an error encountered on the way, if one was encountered. You'll generally want to check for this and handle accordingly:
150
151```coffeescript
152 robot.http("https://midnight-train")
153 .get() (err, response, body) ->
154 if err
155 res.send "Encountered an error :( #{err}"
156 return
157 # your code here, knowing it was successful
158```
159
160`res` is an instance of node's [http.ServerResponse](http://nodejs.org/api/http.html#http_class_http_serverresponse). Most of the methods don't matter as much when using node-scoped-http-client, but of interest are `statusCode` and `getHeader`. Use `statusCode` to check for the HTTP status code, where usually non-200 means something bad happened. Use `getHeader` for peeking at the header, for example to check for rate limiting:
161
162```coffeescript
163 robot.http("https://midnight-train")
164 .get() (err, response, body) ->
165 # pretend there's error checking code here
166
167 if response.statusCode isnt 200
168 res.send "Request didn't come back HTTP 200 :("
169 return
170
171 rateLimitRemaining = parseInt response.getHeader('X-RateLimit-Limit') if response.getHeader('X-RateLimit-Limit')
172 if rateLimitRemaining and rateLimitRemaining < 1
173 res.send "Rate Limit hit, stop believing for awhile"
174
175 # rest of your code
176```
177
178`body` is the response's body as a string, the thing you probably care about the most:
179
180```coffeescript
181 robot.http("https://midnight-train")
182 .get() (err, response, body) ->
183 # error checking code here
184
185 res.send "Got back #{body}"
186```
187
188### JSON
189
190If you are talking to APIs, the easiest way is going to be JSON because it doesn't require any extra dependencies. When making the `robot.http` call, you should usually set the `Accept` header to give the API a clue that's what you are expecting back. Once you get the `body` back, you can parse it with `JSON.parse`:
191
192```coffeescript
193 robot.http("https://midnight-train")
194 .header('Accept', 'application/json')
195 .get() (err, response, body) ->
196 # error checking code here
197
198 data = JSON.parse body
199 res.send "#{data.passenger} taking midnight train going #{data.destination}"
200```
201
202It's possible to get non-JSON back, like if the API hit an error and it tries to render a normal HTML error instead of JSON. To be on the safe side, you should check the `Content-Type`, and catch any errors while parsing.
203
204```coffeescript
205 robot.http("https://midnight-train")
206 .header('Accept', 'application/json')
207 .get() (err, response, body) ->
208 # err & response status checking code here
209
210 if response.getHeader('Content-Type') isnt 'application/json'
211 res.send "Didn't get back JSON :("
212 return
213
214 data = null
215 try
216 data = JSON.parse body
217 catch error
218 res.send "Ran into an error parsing JSON :("
219 return
220
221 # your code here
222```
223
224### XML
225
226XML APIs are harder because there's not a bundled XML parsing library. It's beyond the scope of this documentation to go into detail, but here are a few libraries to check out:
227
228* [xml2json](https://github.com/buglabs/node-xml2json) (simplest to use, but has some limitations)
229* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)
230* [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js)
231
232### Screen scraping
233
234For those times that there isn't an API, there's always the possibility of screen-scraping. It's beyond the scope of this documentation to go into detail, but here's a few libraries to check out:
235
236* [cheerio](https://github.com/MatthewMueller/cheerio) (familiar syntax and API to jQuery)
237* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)
238
239
240### Advanced HTTP and HTTPS settings
241
242As mentioned, hubot uses [node-scoped-http-client](https://github.com/technoweenie/node-scoped-http-client) to provide a simple interface for making HTTP and HTTPS requests. Under its hood, it's using node's builtin [http](http://nodejs.org/api/http.html) and [https](http://nodejs.org/api/https.html) libraries, but providing an easy DSL for the most common kinds of interaction.
243
244If you need to control options on http and https more directly, you pass a second argument to `robot.http` that will be passed on to node-scoped-http-client which will be passed on to http and https:
245
246```
247 options =
248 # don't verify server certificate against a CA, SCARY!
249 rejectUnauthorized: false
250 robot.http("https://midnight-train", options)
251```
252
253In addition, if node-scoped-http-client doesn't suit you, you can use [http](http://nodejs.org/api/http.html) and [https](http://nodejs.org/api/https.html) yourself directly, or any other node library like [request](https://github.com/request/request).
254
255## Random
256
257A common pattern is to hear or respond to commands, and send with a random funny image or line of text from an array of possibilities. It's annoying to do this in JavaScript and CoffeeScript out of the box, so Hubot includes a convenience method:
258
259```coffeescript
260lulz = ['lol', 'rofl', 'lmao']
261
262res.send res.random lulz
263```
264
265## Topic
266
267Hubot can react to a room's topic changing, assuming that the adapter supports it.
268
269```coffeescript
270module.exports = (robot) ->
271 robot.topic (res) ->
272 res.send "#{res.message.text}? That's a Paddlin'"
273```
274
275## Entering and leaving
276
277Hubot can see users entering and leaving, assuming that the adapter supports it.
278
279```coffeescript
280enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
281leaveReplies = ['Are you still there?', 'Target lost', 'Searching']
282
283module.exports = (robot) ->
284 robot.enter (res) ->
285 res.send res.random enterReplies
286 robot.leave (res) ->
287 res.send res.random leaveReplies
288```
289
290## Custom Listeners
291
292While the above helpers cover most of the functionality the average user needs (hear, respond, enter, leave, topic), sometimes you would like to have very specialized matching logic for listeners. If so, you can use `listen` to specify a custom match function instead of a regular expression.
293
294The match function must return a truthy value if the listener callback should be executed. The truthy return value of the match function is then passed to the callback as response.match.
295
296```coffeescript
297module.exports = (robot) ->
298 robot.listen(
299 (message) -> # Match function
300 # only match messages with text (ie ignore enter and other events)
301 return unless message.text
302
303 # Occassionally respond to things that Steve says
304 message.user.name is "Steve" and Math.random() > 0.8
305 (response) -> # Standard listener callback
306 # Let Steve know how happy you are that he exists
307 response.reply "HI STEVE! YOU'RE MY BEST FRIEND! (but only like #{response.match * 100}% of the time)"
308 )
309```
310
311See [the design patterns document](patterns.md#dynamic-matching-of-messages) for examples of complex matchers.
312
313## Environment variables
314
315Hubot can access the environment he's running in, just like any other node program, using [`process.env`](http://nodejs.org/api/process.html#process_process_env). This can be used to configure how scripts are run, with the convention being to use the `HUBOT_` prefix.
316
317```coffeescript
318answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
319
320module.exports = (robot) ->
321 robot.respond /what is the answer to the ultimate question of life/, (res) ->
322 res.send "#{answer}, but what is the question?"
323```
324
325Take care to make sure the script can load if it's not defined, give the Hubot developer notes on how to define it, or default to something. It's up to the script writer to decide if that should be a fatal error (e.g. hubot exits), or not (make any script that relies on it to say it needs to be configured. When possible and when it makes sense to, having a script work without any other configuration is preferred.
326
327Here we can default to something:
328
329```coffeescript
330answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING or 42
331
332module.exports = (robot) ->
333 robot.respond /what is the answer to the ultimate question of life/, (res) ->
334 res.send "#{answer}, but what is the question?"
335```
336
337Here we exit if it's not defined:
338
339```coffeescript
340answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
341unless answer?
342 console.log "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again"
343 process.exit(1)
344
345module.exports = (robot) ->
346 robot.respond /what is the answer to the ultimate question of life/, (res) ->
347 res.send "#{answer}, but what is the question?"
348```
349
350And lastly, we update the `robot.respond` to check it:
351
352```coffeescript
353answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
354
355module.exports = (robot) ->
356 robot.respond /what is the answer to the ultimate question of life/, (res) ->
357 unless answer?
358 res.send "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again"
359 return
360 res.send "#{answer}, but what is the question?"
361```
362
363## Dependencies
364
365Hubot uses [npm](https://github.com/isaacs/npm) to manage its dependencies. To add additional packages, add them to `dependencies` in `package.json`. For example, to add lolimadeupthispackage 1.2.3, it'd look like:
366
367```json
368 "dependencies": {
369 "hubot": "2.5.5",
370 "lolimadeupthispackage": "1.2.3"
371 },
372```
373
374If you are using scripts from hubot-scripts, take note of the `Dependencies` documentation in the script to add. They are listed in a format that can be copy & pasted into `package.json`, just make sure to add commas as necessary to make it valid JSON.
375
376# Timeouts and Intervals
377
378Hubot can run code later using JavaScript's built-in [setTimeout](http://nodejs.org/api/timers.html#timers_settimeout_callback_delay_arg). It takes a callback method, and the amount of time to wait before calling it:
379
380```coffeescript
381module.exports = (robot) ->
382 robot.respond /you are a little slow/, (res) ->
383 setTimeout () ->
384 res.send "Who you calling 'slow'?"
385 , 60 * 1000
386```
387
388Additionally, Hubot can run code on an interval using [setInterval](http://nodejs.org/api/timers.html#timers_setinterval_callback_delay_arg). It takes a callback method, and the amount of time to wait between calls:
389
390```coffeescript
391module.exports = (robot) ->
392 robot.respond /annoy me/, (res) ->
393 res.send "Hey, want to hear the most annoying sound in the world?"
394 setInterval () ->
395 res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
396 , 1000
397```
398
399Both `setTimeout` and `setInterval` return the ID of the timeout or interval it created. This can be used to to `clearTimeout` and `clearInterval`.
400
401```coffeescript
402module.exports = (robot) ->
403 annoyIntervalId = null
404
405 robot.respond /annoy me/, (res) ->
406 if annoyIntervalId
407 res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
408 return
409
410 res.send "Hey, want to hear the most annoying sound in the world?"
411 annoyIntervalId = setInterval () ->
412 res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
413 , 1000
414
415 robot.respond /unannoy me/, (res) ->
416 if annoyIntervalId
417 res.send "GUYS, GUYS, GUYS!"
418 clearInterval(annoyIntervalId)
419 annoyIntervalId = null
420 else
421 res.send "Not annoying you right now, am I?"
422```
423
424## HTTP Listener
425
426Hubot includes support for the [express](http://expressjs.com) web framework to serve up HTTP requests. It listens on the port specified by the `EXPRESS_PORT` or `PORT` environment variables (preferred in that order) and defaults to 8080. An instance of an express application is available at `robot.router`. It can be protected with username and password by specifying `EXPRESS_USER` and `EXPRESS_PASSWORD`. It can automatically serve static files by setting `EXPRESS_STATIC`.
427
428You can increase the [maximum request body size](https://github.com/expressjs/body-parser#limit-3) by specifying `EXPRESS_LIMIT`. It defaults to '100kb'. You can also set the [maximum number of parameters](https://github.com/expressjs/body-parser#parameterlimit) that are allowed in the URL-encoded data by setting `EXPRESS_PARAMETER_LIMIT`. The default is `1000`.
429
430The most common use of this is for providing HTTP end points for services with webhooks to push to, and have those show up in chat.
431
432
433```coffeescript
434module.exports = (robot) ->
435 # the expected value of :room is going to vary by adapter, it might be a numeric id, name, token, or some other value
436 robot.router.post '/hubot/chatsecrets/:room', (request, response) ->
437 room = request.params.room
438 data = if request.body.payload? then JSON.parse request.body.payload else request.body
439 secret = data.secret
440
441 robot.messageRoom room, "I have a secret: #{secret}"
442
443 response.send 'OK'
444```
445
446Test it with curl; also see section on [error handling](#error-handling) below.
447```shell
448// raw json, must specify Content-Type: application/json
449curl -X POST -H "Content-Type: application/json" -d '{"secret":"C-TECH Astronomy"}' http://127.0.0.1:8080/hubot/chatsecrets/general
450
451// defaults Content-Type: application/x-www-form-urlencoded, must st payload=...
452curl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general
453```
454
455All endpoint URLs should start with the literal string `/hubot` (regardless of what your robot's name is). This consistency makes it easier to set up webhooks (copy-pasteable URL) and guarantees that URLs are valid (not all bot names are URL-safe).
456
457## Events
458
459Hubot can also respond to events which can be used to pass data between scripts. This is done by encapsulating node.js's [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) with `robot.emit` and `robot.on`.
460
461One use case for this would be to have one script for handling interactions with a service, and then emitting events as they come up. For example, we could have a script that receives data from a GitHub post-commit hook, make that emit commits as they come in, and then have another script act on those commits.
462
463```coffeescript
464# src/scripts/github-commits.coffee
465module.exports = (robot) ->
466 robot.router.post "/hubot/gh-commits", (request, response) ->
467 robot.emit "commit", {
468 user : {}, #hubot user object
469 repo : 'https://github.com/github/hubot',
470 hash : '2e1951c089bd865839328592ff673d2f08153643'
471 }
472```
473
474```coffeescript
475# src/scripts/heroku.coffee
476module.exports = (robot) ->
477 robot.on "commit", (commit) ->
478 robot.send commit.user, "Will now deploy #{commit.hash} from #{commit.repo}!"
479 #deploy code goes here
480```
481
482If you provide an event, it's highly recommended to include a hubot user or room object in its data. This would allow for hubot to notify a user or room in chat.
483
484## Error Handling
485
486No code is perfect, and errors and exceptions are to be expected. Previously, an uncaught exceptions would crash your hubot instance. Hubot now includes an `uncaughtException` handler, which provides hooks for scripts to do something about exceptions.
487
488```coffeescript
489# src/scripts/does-not-compute.coffee
490module.exports = (robot) ->
491 robot.error (err, res) ->
492 robot.logger.error "DOES NOT COMPUTE"
493
494 if res?
495 res.reply "DOES NOT COMPUTE"
496```
497
498You can do anything you want here, but you will want to take extra precaution of rescuing and logging errors, particularly with asynchronous code. Otherwise, you might find yourself with recursive errors and not know what is going on.
499
500Under the hood, there is an 'error' event emitted, with the error handlers consuming that event. The uncaughtException handler [technically leaves the process in an unknown state](http://nodejs.org/api/process.html#process_event_uncaughtexception). Therefore, you should rescue your own exceptions whenever possible, and emit them yourself. The first argument is the error emitted, and the second argument is an optional message that generated the error.
501
502Using previous examples:
503
504```coffeescript
505 robot.router.post '/hubot/chatsecrets/:room', (request, response) ->
506 room = request.params.room
507 data = null
508 try
509 data = JSON.parse request.body.payload
510 catch err
511 robot.emit 'error', err
512
513 # rest of the code here
514
515
516 robot.hear /midnight train/i, (res) ->
517 robot.http("https://midnight-train")
518 .get() (err, response, body) ->
519 if err
520 res.reply "Had problems taking the midnight train"
521 robot.emit 'error', err, res
522 return
523 # rest of code here
524```
525
526For the second example, it's worth thinking about what messages the user would see. If you have an error handler that replies to the user, you may not need to add a custom message and could send back the error message provided to the `get()` request, but of course it depends on how public you want to be with your exception reporting.
527
528## Documenting Scripts
529
530Hubot scripts can be documented with comments at the top of their file, for example:
531
532```coffeescript
533# Description:
534# <description of the scripts functionality>
535#
536# Dependencies:
537# "<module name>": "<module version>"
538#
539# Configuration:
540# LIST_OF_ENV_VARS_TO_SET
541#
542# Commands:
543# hubot <trigger> - <what the respond trigger does>
544# <trigger> - <what the hear trigger does>
545#
546# Notes:
547# <optional notes required for the script>
548#
549# Author:
550# <github username of the original script author>
551```
552
553The most important and user facing of these is `Commands`. At load time, Hubot looks at the `Commands` section of each scripts, and build a list of all commands. The included `help.coffee` lets a user ask for help across all commands, or with a search. Therefore, documenting the commands make them a lot more discoverable by users.
554
555When documenting commands, here are some best practices:
556
557* Stay on one line. Help commands get sorted, so would insert the second line at an unexpected location, where it probably won't make sense.
558* Refer to the Hubot as hubot, even if your hubot is named something else. It will automatically be replaced with the correct name. This makes it easier to share scripts without having to update docs.
559* For `robot.respond` documentation, always prefix with `hubot`. Hubot will automatically replace this with your robot's name, or the robot's alias if it has one
560* Check out how man pages document themselves. In particular, brackets indicate optional parts, '...' for any number of arguments, etc.
561
562The other sections are more relevant to developers of the bot, particularly dependencies, configuration variables, and notes. All contributions to [hubot-scripts](https://github.com/github/hubot-scripts) should include all these sections that are related to getting up and running with the script.
563
564
565
566## Persistence
567
568Hubot has two persistence methods available that can be
569used to store and retrieve data by scripts: an in-memory key-value store exposed as `robot.brain`, and an optional persistent database-backed key-value store expsoed as `robot.datastore`
570
571### Brain
572
573```coffeescript
574robot.respond /have a soda/i, (res) ->
575 # Get number of sodas had (coerced to a number).
576 sodasHad = robot.brain.get('totalSodas') * 1 or 0
577
578 if sodasHad > 4
579 res.reply "I'm too fizzy.."
580 else
581 res.reply 'Sure!'
582 robot.brain.set 'totalSodas', sodasHad + 1
583
584robot.respond /sleep it off/i, (res) ->
585 robot.brain.set 'totalSodas', 0
586 res.reply 'zzzzz'
587```
588
589If the script needs to lookup user data, there are methods on `robot.brain` for looking up one or many users by id, name, or 'fuzzy' matching of name: `userForName`, `userForId`, `userForFuzzyName`, and `usersForFuzzyName`.
590
591```coffeescript
592module.exports = (robot) ->
593
594 robot.respond /who is @?([\w .\-]+)\?*$/i, (res) ->
595 name = res.match[1].trim()
596
597 users = robot.brain.usersForFuzzyName(name)
598 if users.length is 1
599 user = users[0]
600 # Do something interesting here..
601
602 res.send "#{name} is user - #{user}"
603```
604
605### Datastore
606
607Unlike the brain, the datastore's getter and setter methods are asynchronous and don't resolve until the call to the underlying database has resolved. This requires a slightly different approach to accessing data:
608
609```coffeescript
610robot.respond /have a soda/i, (res) ->
611 # Get number of sodas had (coerced to a number).
612 robot.datastore.get('totalSodas').then (value) ->
613 sodasHad = value * 1 or 0
614
615 if sodasHad > 4
616 res.reply "I'm too fizzy.."
617 else
618 res.reply 'Sure!'
619 robot.brain.set 'totalSodas', sodasHad + 1
620
621robot.respond /sleep it off/i, (res) ->
622 robot.datastore.set('totalSodas', 0).then () ->
623 res.reply 'zzzzz'
624```
625
626The datastore also allows setting and getting values which are scoped to individual users:
627
628```coffeescript
629module.exports = (robot) ->
630
631 robot.respond /who is @?([\w .\-]+)\?*$/i, (res) ->
632 name = res.match[1].trim()
633
634 users = robot.brain.usersForFuzzyName(name)
635 if users.length is 1
636 user = users[0]
637 user.get('roles').then (roles) ->
638 res.send "#{name} is #{roles.join(', ')}"
639```
640
641## Script Loading
642
643There are three main sources to load scripts from:
644
645* all scripts __bundled__ with your hubot installation under `scripts/` directory
646* __community scripts__ specified in `hubot-scripts.json` and shipped in the `hubot-scripts` npm package
647* scripts loaded from external __npm packages__ and specified in `external-scripts.json`
648
649Scripts loaded from the `scripts/` directory are loaded in alphabetical order, so you can expect a consistent load order of scripts. For example:
650
651* `scripts/1-first.coffee`
652* `scripts/_second.coffee`
653* `scripts/third.coffee`
654
655# Sharing Scripts
656
657Once you've built some new scripts to extend the abilities of your robot friend, you should consider sharing them with the world! At the minimum, you need to package up your script and submit it to the [Node.js Package Registry](http://npmjs.org). You should also review the best practices for sharing scripts below.
658
659## See if a script already exists
660
661Start by [checking if an NPM package](index.md#scripts) for a script like yours already exists. If you don't see an existing package that you can contribute to, then you can easily get started using the `hubot` script [yeoman](http://yeoman.io/) generator.
662
663## Creating A Script Package
664
665Creating a script package for hubot is very simple. Start by installing the `hubot` [yeoman](http://yeoman.io/) generator:
666
667
668```
669% npm install -g yo generator-hubot
670```
671
672Once you've got the hubot generator installed, creating a hubot script is similar to creating a new hubot. You create a directory for your hubot script and generate a new `hubot:script` in it. For example, if we wanted to create a hubot script called "my-awesome-script":
673
674```
675% mkdir hubot-my-awesome-script
676% cd hubot-my-awesome-script
677% yo hubot:script
678```
679
680At this point, you'll be asked a few questions about the author of the script, name of the script (which is guessed by the directory name), a short description, and keywords to find it (we suggest having at least `hubot, hubot-scripts` in this list).
681
682If you are using git, the generated directory includes a .gitignore, so you can initialize and add everything:
683
684```
685% git init
686% git add .
687% git commit -m "Initial commit"
688```
689
690You now have a hubot script repository that's ready to roll! Feel free to crack open the pre-created `src/awesome-script.coffee` file and start building up your script! When you've got it ready, you can publish it to [npmjs](http://npmjs.org) by [following their documentation](https://docs.npmjs.com/getting-started/publishing-npm-packages)!
691
692You'll probably want to write some unit tests for your new script. A sample test script is written to
693`test/awesome-script-test.coffee`, which you can run with `grunt`. For more information on tests,
694see the [Testing Hubot Scripts](#testing-hubot-scripts) section.
695
696# Listener Metadata
697
698In addition to a regular expression and callback, the `hear` and `respond` functions also accept an optional options Object which can be used to attach arbitrary metadata to the generated Listener object. This metadata allows for easy extension of your script's behavior without modifying the script package.
699
700The most important and most common metadata key is `id`. Every Listener should be given a unique name (options.id; defaults to `null`). Names should be scoped by module (e.g. 'my-module.my-listener'). These names allow other scripts to directly address individual listeners and extend them with additional functionality like authorization and rate limiting.
701
702Additional extensions may define and handle additional metadata keys. For more information, see the [Listener Middleware section](#listener-middleware).
703
704Returning to an earlier example:
705
706```coffeescript
707module.exports = (robot) ->
708 robot.respond /annoy me/, id:'annoyance.start', (res)
709 # code to annoy someone
710
711 robot.respond /unannoy me/, id:'annoyance.stop', (res)
712 # code to stop annoying someone
713```
714
715These scoped identifiers allow you to externally specify new behaviors like:
716- authorization policy: "allow everyone in the `annoyers` group to execute `annoyance.*` commands"
717- rate limiting: "only allow executing `annoyance.start` once every 30 minutes"
718
719# Middleware
720
721There are three kinds of middleware: Receive, Listener and Response.
722
723Receive middleware runs once, before listeners are checked.
724Listener middleware runs for every listener that matches the message.
725Response middleware runs for every response sent to a message.
726
727## Execution Process and API
728
729Similar to [Express middleware](http://expressjs.com/api.html#middleware), Hubot executes middleware in definition order. Each middleware can either continue the chain (by calling `next`) or interrupt the chain (by calling `done`). If all middleware continues, the listener callback is executed and `done` is called. Middleware may wrap the `done` callback to allow executing code in the second half of the process (after the listener callback has been executed or a deeper piece of middleware has interrupted).
730
731Middleware is called with:
732
733- `context`
734 - See the each middleware type's API to see what the context will expose.
735- `next`
736 - a Function with no additional properties that should be called to continue on to the next piece of middleware/execute the Listener callback
737 - `next` should be called with a single, optional argument: either the provided `done` function or a new function that eventually calls `done`. If the argument is not given, the provided `done` will be assumed.
738- `done`
739 - a Function with no additional properties that should be called to interrupt middleware execution and begin executing the chain of completion functions.
740 - `done` should be called with no arguments
741
742Every middleware receives the same API signature of `context`, `next`, and
743`done`. Different kinds of middleware may receive different information in the
744`context` object. For more details, see the API for each type of middleware.
745
746### Error Handling
747
748For synchronous middleware (never yields to the event loop), hubot will automatically catch errors and emit an an `error` event, just like in standard listeners. Hubot will also automatically call the most recent `done` callback to unwind the middleware stack. Asynchronous middleware should catch its own exceptions, emit an `error` event, and call `done`. Any uncaught exceptions will interrupt all execution of middleware completion callbacks.
749
750# Listener Middleware
751
752Listener middleware inserts logic between the listener matching a message and the listener executing. This allows you to create extensions that run for every matching script. Examples include centralized authorization policies, rate limiting, logging, and metrics. Middleware is implemented like other hubot scripts: instead of using the `hear` and `respond` methods, middleware is registered using `listenerMiddleware`.
753
754## Listener Middleware Examples
755
756A fully functioning example can be found in [hubot-rate-limit](https://github.com/michaelansel/hubot-rate-limit/blob/master/src/rate-limit.coffee).
757
758A simple example of middleware logging command executions:
759
760```coffeescript
761module.exports = (robot) ->
762 robot.listenerMiddleware (context, next, done) ->
763 # Log commands
764 robot.logger.info "#{context.response.message.user.name} asked me to #{context.response.message.text}"
765 # Continue executing middleware
766 next()
767```
768
769In this example, a log message will be written for each chat message that matches a Listener.
770
771A more complex example making a rate limiting decision:
772
773```coffeescript
774module.exports = (robot) ->
775 # Map of listener ID to last time it was executed
776 lastExecutedTime = {}
777
778 robot.listenerMiddleware (context, next, done) ->
779 try
780 # Default to 1s unless listener provides a different minimum period
781 minPeriodMs = context.listener.options?.rateLimits?.minPeriodMs? or 1000
782
783 # See if command has been executed recently
784 if lastExecutedTime.hasOwnProperty(context.listener.options.id) and
785 lastExecutedTime[context.listener.options.id] > Date.now() - minPeriodMs
786 # Command is being executed too quickly!
787 done()
788 else
789 next ->
790 lastExecutedTime[context.listener.options.id] = Date.now()
791 done()
792 catch err
793 robot.emit('error', err, context.response)
794```
795
796In this example, the middleware checks to see if the listener has been executed in the last 1,000ms. If it has, the middleware calls `done` immediately, preventing the listener callback from being called. If the listener is allowed to execute, the middleware attaches a `done` handler so that it can record the time the listener *finished* executing.
797
798This example also shows how listener-specific metadata can be leveraged to create very powerful extensions: a script developer can use the rate limiting middleware to easily rate limit commands at different rates by just adding the middleware and setting a listener option.
799
800```coffeescript
801module.exports = (robot) ->
802 robot.hear /hello/, id: 'my-hello', rateLimits: {minPeriodMs: 10000}, (res) ->
803 # This will execute no faster than once every ten seconds
804 res.reply 'Why, hello there!'
805```
806
807## Listener Middleware API
808
809Listener middleware callbacks receive three arguments, `context`, `next`, and
810`done`. See the [middleware API](#execution-process-and-api) for a description
811of `next` and `done`. Listener middleware context includes these fields:
812 - `listener`
813 - `options`: a simple Object containing options set when defining the listener. See [Listener Metadata](#listener-metadata).
814 - all other properties should be considered internal
815 - `response`
816 - all parts of the standard response API are included in the middleware API. See [Send & Reply](#send--reply).
817 - middleware may decorate (but not modify) the response object with additional information (e.g. add a property to `response.message.user` with a user's LDAP groups)
818 - note: the textual message (`response.message.text`) should be considered immutable in listener middleware
819
820# Receive Middleware
821
822Receive middleware runs before any listeners have executed. It's suitable for
823blacklisting commands that have not been updated to add an ID, metrics, and more.
824
825## Receive Middleware Example
826
827This simple middlware bans hubot use by a particular user, including `hear`
828listeners. If the user attempts to run a command explicitly, it will return
829an error message.
830
831```coffeescript
832BLACKLISTED_USERS = [
833 '12345' # Restrict access for a user ID for a contractor
834]
835
836robot.receiveMiddleware (context, next, done) ->
837 if context.response.message.user.id in BLACKLISTED_USERS
838 # Don't process this message further.
839 context.response.message.finish()
840
841 # If the message starts with 'hubot' or the alias pattern, this user was
842 # explicitly trying to run a command, so respond with an error message.
843 if context.response.message.text?.match(robot.respondPattern(''))
844 context.response.reply "I'm sorry @#{context.response.message.user.name}, but I'm configured to ignore your commands."
845
846 # Don't process further middleware.
847 done()
848 else
849 next(done)
850```
851
852## Receive Middleware API
853
854Receive middleware callbacks receive three arguments, `context`, `next`, and
855`done`. See the [middleware API](#execution-process-and-api) for a description
856of `next` and `done`. Receive middleware context includes these fields:
857 - `response`
858 - this response object will not have a `match` property, as no listeners have been run yet to match it.
859 - middleware may decorate the response object with additional information (e.g. add a property to `response.message.user` with a user's LDAP groups)
860 - middleware may modify the `response.message` object
861
862# Response Middleware
863
864Response middleware runs against every message hubot sends to a chat room. It's
865helpful for message formatting, preventing password leaks, metrics, and more.
866
867## Response Middleware Example
868
869This simple example changes the format of links sent to a chat room from
870markdown links (like [example](https://example.com)) to the format supported
871by [Slack](https://slack.com), <https://example.com|example>.
872
873```coffeescript
874module.exports = (robot) ->
875 robot.responseMiddleware (context, next, done) ->
876 return unless context.plaintext?
877 context.strings = (string.replace(/\[([^\[\]]*?)\]\((https?:\/\/.*?)\)/, "<$2|$1>") for string in context.strings)
878 next()
879```
880
881## Response Middleware API
882
883Response middleware callbacks receive three arguments, `context`, `next`, and
884`done`. See the [middleware API](#execution-process-and-api) for a description
885of `next` and `done`. Receive middleware context includes these fields:
886 - `response`
887 - This response object can be used to send new messages from the middleware. Middleware will be called on these new responses. Be careful not to create infinite loops.
888 - `strings`
889 - An array of strings being sent to the chat room adapter. You can edit these, or use `context.strings = ["new strings"]` to replace them.
890 - `method`
891 - A string representing which type of response message the listener sent, such as `send`, `reply`, `emote` or `topic`.
892 - `plaintext`
893 - `true` or `undefined`. This will be set to `true` if the message is of a normal plaintext type, such as `send` or `reply`. This property should be treated as read-only.
894
895# Testing Hubot Scripts
896
897[hubot-test-helper](https://github.com/mtsmfm/hubot-test-helper) is a good
898framework for unit testing Hubot scripts. (Note that, in order to use
899hubot-test-helper, you'll need a recent Node version with support for Promises.)
900
901Install the package in your Hubot instance:
902
903``` % npm install hubot-test-helper --save-dev ```
904
905You'll also need to install:
906
907 * a JavaScript testing framework such as *Mocha*
908 * an assertion library such as *chai* or *expect.js*
909
910You may also want to install:
911
912 * *coffeescript* (if you're writing your tests in CoffeeScript rather than JavaScript)
913 * a mocking library such as *Sinon.js* (if your script performs webservice calls or
914 other asynchronous actions)
915
916Here is a sample script that tests the first couple of commands in the
917[Hubot sample script](https://github.com/hubotio/generator-hubot/blob/master/generators/app/templates/scripts/example.coffee). This script uses *Mocha*, *chai*, *coffeescript*, and of course *hubot-test-helper*:
918
919**test/example-test.coffee**
920```coffeescript
921Helper = require('hubot-test-helper')
922chai = require 'chai'
923
924expect = chai.expect
925
926helper = new Helper('../scripts/example.coffee')
927
928describe 'example script', ->
929 beforeEach ->
930 @room = helper.createRoom()
931
932 afterEach ->
933 @room.destroy()
934
935 it 'doesn\'t need badgers', ->
936 @room.user.say('alice', 'did someone call for a badger?').then =>
937 expect(@room.messages).to.eql [
938 ['alice', 'did someone call for a badger?']
939 ['hubot', 'Badgers? BADGERS? WE DON\'T NEED NO STINKIN BADGERS']
940 ]
941
942 it 'won\'t open the pod bay doors', ->
943 @room.user.say('bob', '@hubot open the pod bay doors').then =>
944 expect(@room.messages).to.eql [
945 ['bob', '@hubot open the pod bay doors']
946 ['hubot', '@bob I\'m afraid I can\'t let you do that.']
947 ]
948
949 it 'will open the dutch doors', ->
950 @room.user.say('bob', '@hubot open the dutch doors').then =>
951 expect(@room.messages).to.eql [
952 ['bob', '@hubot open the dutch doors']
953 ['hubot', '@bob Opening dutch doors']
954 ]
955```
956
957**sample output**
958```bash
959% mocha --require coffeescript/register test/*.coffee
960
961
962 example script
963 ✓ doesn't need badgers
964 ✓ won't open the pod bay doors
965 ✓ will open the dutch doors
966
967
968 3 passing (212ms)
969```