1 | ---
|
2 | permalink: /docs/scripting/
|
3 | ---
|
4 |
|
5 | # Scripting
|
6 |
|
7 | Hubot 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 |
|
11 | When 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 |
|
17 | By export a function, we just mean:
|
18 |
|
19 | ```coffeescript
|
20 | module.exports = (robot) ->
|
21 | # your code here
|
22 | ```
|
23 |
|
24 | The `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 |
|
28 | Since 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
|
31 | module.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 |
|
39 | The `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 |
|
45 | The `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 |
|
52 | It 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 |
|
61 | The `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
|
64 | module.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 |
|
75 | The `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 |
|
77 | If 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 |
|
81 | Messages can be sent to a specified room or user using the messageRoom function.
|
82 |
|
83 | ```coffeescript
|
84 | module.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 |
|
91 | User name can be explicitely specified if desired ( for a cc to an admin/manager), or using
|
92 | the 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 |
|
107 | So 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 |
|
114 | If 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 |
|
127 | Hubot 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 |
|
136 | A 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 |
|
190 | If 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 |
|
202 | It'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 |
|
226 | XML 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 |
|
234 | For 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 |
|
242 | As 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 |
|
244 | If 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 |
|
253 | In 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 |
|
257 | A 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
|
260 | lulz = ['lol', 'rofl', 'lmao']
|
261 |
|
262 | res.send res.random lulz
|
263 | ```
|
264 |
|
265 | ## Topic
|
266 |
|
267 | Hubot can react to a room's topic changing, assuming that the adapter supports it.
|
268 |
|
269 | ```coffeescript
|
270 | module.exports = (robot) ->
|
271 | robot.topic (res) ->
|
272 | res.send "#{res.message.text}? That's a Paddlin'"
|
273 | ```
|
274 |
|
275 | ## Entering and leaving
|
276 |
|
277 | Hubot can see users entering and leaving, assuming that the adapter supports it.
|
278 |
|
279 | ```coffeescript
|
280 | enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
|
281 | leaveReplies = ['Are you still there?', 'Target lost', 'Searching']
|
282 |
|
283 | module.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 |
|
292 | While 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 |
|
294 | The 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
|
297 | module.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 |
|
311 | See [the design patterns document](patterns.md#dynamic-matching-of-messages) for examples of complex matchers.
|
312 |
|
313 | ## Environment variables
|
314 |
|
315 | Hubot 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
|
318 | answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
319 |
|
320 | module.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 |
|
325 | Take 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 |
|
327 | Here we can default to something:
|
328 |
|
329 | ```coffeescript
|
330 | answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING or 42
|
331 |
|
332 | module.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 |
|
337 | Here we exit if it's not defined:
|
338 |
|
339 | ```coffeescript
|
340 | answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
341 | unless 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 |
|
345 | module.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 |
|
350 | And lastly, we update the `robot.respond` to check it:
|
351 |
|
352 | ```coffeescript
|
353 | answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
354 |
|
355 | module.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 |
|
365 | Hubot 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 |
|
374 | If 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 |
|
378 | Hubot 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
|
381 | module.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 |
|
388 | Additionally, 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
|
391 | module.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 |
|
399 | Both `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
|
402 | module.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 |
|
426 | Hubot 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 |
|
428 | You 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 |
|
430 | The 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
|
434 | module.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 |
|
446 | Test it with curl; also see section on [error handling](#error-handling) below.
|
447 | ```shell
|
448 | // raw json, must specify Content-Type: application/json
|
449 | curl -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=...
|
452 | curl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general
|
453 | ```
|
454 |
|
455 | All 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 |
|
459 | Hubot 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 |
|
461 | One 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
|
465 | module.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
|
476 | module.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 |
|
482 | If 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 |
|
486 | No 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
|
490 | module.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 |
|
498 | You 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 |
|
500 | Under 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 |
|
502 | Using 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 |
|
526 | For 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 |
|
530 | Hubot 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 |
|
553 | The 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 |
|
555 | When 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 |
|
562 | The 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 |
|
568 | Hubot has two persistence methods available that can be
|
569 | used 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
|
574 | robot.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 |
|
584 | robot.respond /sleep it off/i, (res) ->
|
585 | robot.brain.set 'totalSodas', 0
|
586 | res.reply 'zzzzz'
|
587 | ```
|
588 |
|
589 | If 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
|
592 | module.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 |
|
607 | Unlike 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
|
610 | robot.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 |
|
621 | robot.respond /sleep it off/i, (res) ->
|
622 | robot.datastore.set('totalSodas', 0).then () ->
|
623 | res.reply 'zzzzz'
|
624 | ```
|
625 |
|
626 | The datastore also allows setting and getting values which are scoped to individual users:
|
627 |
|
628 | ```coffeescript
|
629 | module.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 |
|
643 | There 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 |
|
649 | Scripts 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 |
|
657 | Once 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 |
|
661 | Start 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 |
|
665 | Creating 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 |
|
672 | Once 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 |
|
680 | At 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 |
|
682 | If 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 |
|
690 | You 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 |
|
692 | You'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,
|
694 | see the [Testing Hubot Scripts](#testing-hubot-scripts) section.
|
695 |
|
696 | # Listener Metadata
|
697 |
|
698 | In 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 |
|
700 | The 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 |
|
702 | Additional extensions may define and handle additional metadata keys. For more information, see the [Listener Middleware section](#listener-middleware).
|
703 |
|
704 | Returning to an earlier example:
|
705 |
|
706 | ```coffeescript
|
707 | module.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 |
|
715 | These 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 |
|
721 | There are three kinds of middleware: Receive, Listener and Response.
|
722 |
|
723 | Receive middleware runs once, before listeners are checked.
|
724 | Listener middleware runs for every listener that matches the message.
|
725 | Response middleware runs for every response sent to a message.
|
726 |
|
727 | ## Execution Process and API
|
728 |
|
729 | Similar 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 |
|
731 | Middleware 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 |
|
742 | Every 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 |
|
748 | For 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 |
|
752 | Listener 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 |
|
756 | A fully functioning example can be found in [hubot-rate-limit](https://github.com/michaelansel/hubot-rate-limit/blob/master/src/rate-limit.coffee).
|
757 |
|
758 | A simple example of middleware logging command executions:
|
759 |
|
760 | ```coffeescript
|
761 | module.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 |
|
769 | In this example, a log message will be written for each chat message that matches a Listener.
|
770 |
|
771 | A more complex example making a rate limiting decision:
|
772 |
|
773 | ```coffeescript
|
774 | module.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 |
|
796 | In 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 |
|
798 | This 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
|
801 | module.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 |
|
809 | Listener middleware callbacks receive three arguments, `context`, `next`, and
|
810 | `done`. See the [middleware API](#execution-process-and-api) for a description
|
811 | of `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 |
|
822 | Receive middleware runs before any listeners have executed. It's suitable for
|
823 | blacklisting commands that have not been updated to add an ID, metrics, and more.
|
824 |
|
825 | ## Receive Middleware Example
|
826 |
|
827 | This simple middlware bans hubot use by a particular user, including `hear`
|
828 | listeners. If the user attempts to run a command explicitly, it will return
|
829 | an error message.
|
830 |
|
831 | ```coffeescript
|
832 | BLACKLISTED_USERS = [
|
833 | '12345' # Restrict access for a user ID for a contractor
|
834 | ]
|
835 |
|
836 | robot.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 |
|
854 | Receive middleware callbacks receive three arguments, `context`, `next`, and
|
855 | `done`. See the [middleware API](#execution-process-and-api) for a description
|
856 | of `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 |
|
864 | Response middleware runs against every message hubot sends to a chat room. It's
|
865 | helpful for message formatting, preventing password leaks, metrics, and more.
|
866 |
|
867 | ## Response Middleware Example
|
868 |
|
869 | This simple example changes the format of links sent to a chat room from
|
870 | markdown links (like [example](https://example.com)) to the format supported
|
871 | by [Slack](https://slack.com), <https://example.com|example>.
|
872 |
|
873 | ```coffeescript
|
874 | module.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 |
|
883 | Response middleware callbacks receive three arguments, `context`, `next`, and
|
884 | `done`. See the [middleware API](#execution-process-and-api) for a description
|
885 | of `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
|
898 | framework for unit testing Hubot scripts. (Note that, in order to use
|
899 | hubot-test-helper, you'll need a recent Node version with support for Promises.)
|
900 |
|
901 | Install the package in your Hubot instance:
|
902 |
|
903 | ``` % npm install hubot-test-helper --save-dev ```
|
904 |
|
905 | You'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 |
|
910 | You 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 |
|
916 | Here 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
|
921 | Helper = require('hubot-test-helper')
|
922 | chai = require 'chai'
|
923 |
|
924 | expect = chai.expect
|
925 |
|
926 | helper = new Helper('../scripts/example.coffee')
|
927 |
|
928 | describe '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 | ```
|