hubot 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +36 -0
- package/.node-version +1 -0
- package/README.md +4 -1
- package/bin/hubot +1 -1
- package/docs/adapters/development.md +35 -36
- package/docs/implementation.md +1 -3
- package/docs/index.md +5 -10
- package/docs/patterns.md +132 -101
- package/docs/scripting.md +493 -411
- package/package.json +19 -18
- package/src/adapters/shell.js +4 -9
- package/src/robot.js +2 -2
- package/test/datastore_test.js +4 -0
- package/test/robot_test.js +2 -2
- package/test/shell_test.js +72 -0
- package/.env.example +0 -2
- package/.envrc +0 -5
- package/.travis.yml +0 -27
package/docs/scripting.md
CHANGED
|
@@ -4,21 +4,22 @@ permalink: /docs/scripting/
|
|
|
4
4
|
|
|
5
5
|
# Scripting
|
|
6
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.
|
|
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
8
|
|
|
9
9
|
## Anatomy of a script
|
|
10
10
|
|
|
11
|
-
When you created your
|
|
11
|
+
When you created your Hubot, the generator also created a `scripts` directory. If you peek around there, you will see some examples. For a script to be a script, it needs to:
|
|
12
12
|
|
|
13
|
-
* live in a directory on the
|
|
14
|
-
* be a `.
|
|
15
|
-
* export a function
|
|
13
|
+
* live in a directory on the Hubot script load path (`src/scripts` and `scripts` by default)
|
|
14
|
+
* be a `.js` file
|
|
15
|
+
* export a function whos signature takes 1 parameter (`robot`)
|
|
16
16
|
|
|
17
17
|
By export a function, we just mean:
|
|
18
18
|
|
|
19
|
-
```
|
|
20
|
-
module.exports = (robot)
|
|
21
|
-
|
|
19
|
+
```javascript
|
|
20
|
+
module.exports = (robot) => {
|
|
21
|
+
// your code here
|
|
22
|
+
}
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
The `robot` parameter is an instance of your robot friend. At this point, we can start scripting up some awesomeness.
|
|
@@ -27,22 +28,25 @@ The `robot` parameter is an instance of your robot friend. At this point, we can
|
|
|
27
28
|
|
|
28
29
|
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
|
|
|
30
|
-
```
|
|
31
|
-
module.exports = (robot)
|
|
32
|
-
robot.hear
|
|
33
|
-
|
|
31
|
+
```javascript
|
|
32
|
+
module.exports = (robot) => {
|
|
33
|
+
robot.hear(/badger/i, (res) => {
|
|
34
|
+
// your code here
|
|
35
|
+
})
|
|
34
36
|
|
|
35
|
-
robot.respond
|
|
36
|
-
|
|
37
|
+
robot.respond(/open the pod bay doors/i, (res) => {
|
|
38
|
+
// your code here
|
|
39
|
+
}
|
|
40
|
+
}
|
|
37
41
|
```
|
|
38
42
|
|
|
39
|
-
The `robot.hear
|
|
43
|
+
The `robot.hear(/badger/)` callback is called anytime a message's text matches. For example:
|
|
40
44
|
|
|
41
45
|
* Stop badgering the witness
|
|
42
46
|
* badger me
|
|
43
47
|
* what exactly is a badger anyways
|
|
44
48
|
|
|
45
|
-
The `robot.respond
|
|
49
|
+
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
50
|
|
|
47
51
|
* hal open the pod bay doors
|
|
48
52
|
* HAL: open the pod bay doors
|
|
@@ -52,178 +56,187 @@ The `robot.respond /open the pod bay doors/i` callback is only called for messag
|
|
|
52
56
|
It wouldn't be called for:
|
|
53
57
|
|
|
54
58
|
* HAL: please open the pod bay doors
|
|
55
|
-
* because its `respond` is
|
|
59
|
+
* because its `respond` is expecting the text to be prefixed with the robots name
|
|
56
60
|
* has anyone ever mentioned how lovely you are when you open the pod bay doors?
|
|
57
|
-
* because it lacks the robot's name
|
|
61
|
+
* because it lacks the robot's name at the beginning
|
|
58
62
|
|
|
59
63
|
## Send & reply
|
|
60
64
|
|
|
61
65
|
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
66
|
|
|
63
|
-
```
|
|
64
|
-
module.exports = (robot)
|
|
65
|
-
robot.hear
|
|
66
|
-
res.send
|
|
67
|
+
```javascript
|
|
68
|
+
module.exports = (robot) => {
|
|
69
|
+
robot.hear(/badger/i, (res) => {
|
|
70
|
+
res.send(`Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS`)
|
|
71
|
+
}
|
|
67
72
|
|
|
68
|
-
robot.respond
|
|
69
|
-
res.reply
|
|
73
|
+
robot.respond(/open the pod bay doors/i, (res) => {
|
|
74
|
+
res.reply(`I'm afraid I can't let you do that.`)
|
|
75
|
+
}
|
|
70
76
|
|
|
71
|
-
robot.hear
|
|
72
|
-
res.emote
|
|
77
|
+
robot.hear(/I like pie/i, (res) => {
|
|
78
|
+
res.emote('makes a freshly baked pie')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
73
81
|
```
|
|
74
82
|
|
|
75
|
-
The `robot.hear
|
|
83
|
+
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
84
|
|
|
77
|
-
If a user Dave says "HAL: open the pod bay doors", `robot.respond
|
|
85
|
+
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
86
|
|
|
79
87
|
## Messages to a room or user
|
|
80
88
|
|
|
81
89
|
Messages can be sent to a specified room or user using the messageRoom function.
|
|
82
90
|
|
|
83
|
-
```
|
|
84
|
-
module.exports = (robot)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
room
|
|
88
|
-
|
|
91
|
+
```javascript
|
|
92
|
+
module.exports = (robot) => {
|
|
93
|
+
robot.hear(/green eggs/i, (res) => {
|
|
94
|
+
const room = 'mytestroom'
|
|
95
|
+
robot.messageRoom(room, 'I do not like green eggs and ham. I do not like them Sam-I-Am.')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
89
98
|
```
|
|
90
99
|
|
|
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.
|
|
100
|
+
User name can be explicitely specified if desired ( for a cc to an admin/manager), or using the response object a private message can be sent to the original sender.
|
|
93
101
|
|
|
94
|
-
```
|
|
95
|
-
robot.respond
|
|
96
|
-
room =
|
|
97
|
-
robot.messageRoom
|
|
98
|
-
res.reply
|
|
102
|
+
```javascript
|
|
103
|
+
robot.respond(/I don't like sam-i-am/i, (res) => {
|
|
104
|
+
const room = 'joemanager'
|
|
105
|
+
robot.messageRoom(room, 'Someone does not like Dr. Seus')
|
|
106
|
+
res.reply('That Sam-I-Am\nThat Sam-I-Am\nI do not like\nthat Sam-I-Am')
|
|
107
|
+
}
|
|
99
108
|
|
|
100
|
-
robot.hear
|
|
101
|
-
room =
|
|
102
|
-
robot.messageRoom
|
|
109
|
+
robot.hear(/Sam-I-Am/i, (res) => {
|
|
110
|
+
const room = res.envelope.user.name
|
|
111
|
+
robot.messageRoom(room, 'That Sam-I-Am\nThat Sam-I-Am\nI do not like\nthat Sam-I-Am')
|
|
112
|
+
}
|
|
103
113
|
```
|
|
104
114
|
|
|
105
115
|
## Capturing data
|
|
106
116
|
|
|
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:
|
|
117
|
+
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 on `res.match`. For example, if we update a script like:
|
|
108
118
|
|
|
109
|
-
```
|
|
110
|
-
robot.respond
|
|
111
|
-
|
|
119
|
+
```javascript
|
|
120
|
+
robot.respond(/open the (.*) doors/i, (res) => {
|
|
121
|
+
// your code here
|
|
122
|
+
}
|
|
112
123
|
```
|
|
113
124
|
|
|
114
125
|
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
126
|
|
|
116
|
-
```
|
|
117
|
-
robot.respond
|
|
118
|
-
doorType = res.match[1]
|
|
119
|
-
if doorType
|
|
120
|
-
res.reply
|
|
121
|
-
else
|
|
122
|
-
res.reply
|
|
127
|
+
```javascript
|
|
128
|
+
robot.respond(/open the (.*) doors/i, (res) => {
|
|
129
|
+
const doorType = res.match[1]
|
|
130
|
+
if (doorType == 'pod bay') {
|
|
131
|
+
res.reply(`I'm afraid I can't let you do that.`)
|
|
132
|
+
} else {
|
|
133
|
+
res.reply(`Opening ${doorType} doors`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
123
136
|
```
|
|
124
137
|
|
|
125
|
-
## Making HTTP calls
|
|
138
|
+
## Making HTTP calls (please use `fetch` instead)
|
|
126
139
|
|
|
127
|
-
Hubot can make HTTP calls on your behalf to integrate & consume third party APIs. This can be through an instance of [
|
|
140
|
+
Hubot can make HTTP calls on your behalf to integrate & consume third party APIs. This can be through an instance of [ScopedHttpClient](../src/httpclient.js) available at `robot.http`. The simplest case looks like:
|
|
128
141
|
|
|
129
142
|
|
|
130
|
-
```
|
|
131
|
-
robot.http(
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
```javascript
|
|
144
|
+
robot.http('https://midnight-train').get()((err, res, body) => {
|
|
145
|
+
// your code here
|
|
146
|
+
})
|
|
134
147
|
```
|
|
135
148
|
|
|
136
149
|
A post looks like:
|
|
137
150
|
|
|
138
|
-
```
|
|
139
|
-
data = JSON.stringify({
|
|
151
|
+
```javascript
|
|
152
|
+
const data = JSON.stringify({
|
|
140
153
|
foo: 'bar'
|
|
141
154
|
})
|
|
142
|
-
robot.http(
|
|
155
|
+
robot.http('https://midnight-train')
|
|
143
156
|
.header('Content-Type', 'application/json')
|
|
144
|
-
.post(data)
|
|
145
|
-
|
|
157
|
+
.post(data)((err, res, body) => {
|
|
158
|
+
// your code here
|
|
159
|
+
})
|
|
146
160
|
```
|
|
147
161
|
|
|
148
162
|
|
|
149
163
|
`err` is an error encountered on the way, if one was encountered. You'll generally want to check for this and handle accordingly:
|
|
150
164
|
|
|
151
|
-
```
|
|
152
|
-
robot.http(
|
|
153
|
-
.get()
|
|
154
|
-
if err
|
|
155
|
-
res.send
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
```javascript
|
|
166
|
+
robot.http('https://midnight-train')
|
|
167
|
+
.get()((err, res, body) => {
|
|
168
|
+
if (err){
|
|
169
|
+
return res.send `Encountered an error :( ${err}`
|
|
170
|
+
}
|
|
171
|
+
// your code here, knowing it was successful
|
|
172
|
+
})
|
|
158
173
|
```
|
|
159
174
|
|
|
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
|
|
161
|
-
|
|
162
|
-
```coffeescript
|
|
163
|
-
robot.http("https://midnight-train")
|
|
164
|
-
.get() (err, response, body) ->
|
|
165
|
-
# pretend there's error checking code here
|
|
175
|
+
`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 `ScopedHttpClient`, 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:
|
|
166
176
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
```javascript
|
|
178
|
+
robot.http('https://midnight-train')
|
|
179
|
+
.get() ((err, res, body) => {
|
|
180
|
+
// pretend there's error checking code here
|
|
181
|
+
if (res.statusCode <> 200)
|
|
182
|
+
return res.send(`Request didn't come back HTTP 200 :(`)
|
|
170
183
|
|
|
171
|
-
rateLimitRemaining =
|
|
172
|
-
if rateLimitRemaining
|
|
173
|
-
res.send
|
|
184
|
+
const rateLimitRemaining = res.getHeader('X-RateLimit-Limit') ? parseInt(res.getHeader('X-RateLimit-Limit')) : 1
|
|
185
|
+
if (rateLimitRemaining && rateLimitRemaining < 1)
|
|
186
|
+
return res.send('Rate Limit hit, stop believing for awhile')
|
|
174
187
|
|
|
175
|
-
|
|
188
|
+
// rest of your code
|
|
189
|
+
}
|
|
176
190
|
```
|
|
177
191
|
|
|
178
192
|
`body` is the response's body as a string, the thing you probably care about the most:
|
|
179
193
|
|
|
180
|
-
```
|
|
181
|
-
robot.http(
|
|
182
|
-
.get()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
```javascript
|
|
195
|
+
robot.http('https://midnight-train')
|
|
196
|
+
.get()((err, res, body) => {
|
|
197
|
+
// error checking code here
|
|
198
|
+
res.send(`Got back ${body}`)
|
|
199
|
+
})
|
|
186
200
|
```
|
|
187
201
|
|
|
188
202
|
### JSON
|
|
189
203
|
|
|
190
|
-
If you are talking to
|
|
204
|
+
If you are talking to Web Services that respond with JSON representation, then when making the `robot.http` call, you will usually set the `Accept` header to give the Web Service a clue that's what you are expecting back. Once you get the `body` back, you can parse it with `JSON.parse`:
|
|
191
205
|
|
|
192
|
-
```
|
|
193
|
-
robot.http(
|
|
206
|
+
```javascript
|
|
207
|
+
robot.http('https://midnight-train')
|
|
194
208
|
.header('Accept', 'application/json')
|
|
195
|
-
.get()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
data
|
|
199
|
-
|
|
209
|
+
.get()((err, res, body) => {
|
|
210
|
+
// error checking code here
|
|
211
|
+
const data = JSON.parse(body)
|
|
212
|
+
res.send(`${data.passenger} taking midnight train going ${data.destination}`)
|
|
213
|
+
})
|
|
200
214
|
```
|
|
201
215
|
|
|
202
|
-
It's possible to get non-JSON back, like if the
|
|
216
|
+
It's possible to get non-JSON back, like if the Web Service has an error and renders HTML instead of JSON. To be on the safe side, you should check the `Content-Type`, and catch any errors while parsing.
|
|
203
217
|
|
|
204
|
-
```
|
|
205
|
-
robot.http(
|
|
218
|
+
```javascript
|
|
219
|
+
robot.http('https://midnight-train')
|
|
206
220
|
.header('Accept', 'application/json')
|
|
207
|
-
.get()
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
# your code here
|
|
221
|
+
.get()((err, res, body) => {
|
|
222
|
+
// err & res status checking code here
|
|
223
|
+
if (res.getHeader('Content-Type') != 'application/json'){
|
|
224
|
+
return res.send(`Didn't get back JSON :(`)
|
|
225
|
+
}
|
|
226
|
+
let data = null
|
|
227
|
+
try {
|
|
228
|
+
data = JSON.parse(body)
|
|
229
|
+
} catch (error) {
|
|
230
|
+
res.send(`Ran into an error parsing JSON :(`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// your code here
|
|
234
|
+
})
|
|
222
235
|
```
|
|
223
236
|
|
|
224
237
|
### XML
|
|
225
238
|
|
|
226
|
-
XML
|
|
239
|
+
XML Web Services require installing a 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
240
|
|
|
228
241
|
* [xml2json](https://github.com/buglabs/node-xml2json) (simplest to use, but has some limitations)
|
|
229
242
|
* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)
|
|
@@ -231,7 +244,7 @@ XML APIs are harder because there's not a bundled XML parsing library. It's beyo
|
|
|
231
244
|
|
|
232
245
|
### Screen scraping
|
|
233
246
|
|
|
234
|
-
For
|
|
247
|
+
For consuming a Web Service that responds with HTML, you'll need an HTML parser. It's beyond the scope of this documentation to go into detail, but here's a few libraries to check out:
|
|
235
248
|
|
|
236
249
|
* [cheerio](https://github.com/MatthewMueller/cheerio) (familiar syntax and API to jQuery)
|
|
237
250
|
* [jsdom](https://github.com/tmpvar/jsdom) (JavaScript implementation of the W3C DOM)
|
|
@@ -239,125 +252,143 @@ For those times that there isn't an API, there's always the possibility of scree
|
|
|
239
252
|
|
|
240
253
|
### Advanced HTTP and HTTPS settings
|
|
241
254
|
|
|
242
|
-
As mentioned,
|
|
255
|
+
As mentioned previously, Hubot uses [ScopedHttpClient](../src/httpclient.js) to provide a simple interface for making HTTP and HTTPS requests. Under the hood, it's using node's [http](http://nodejs.org/api/http.html) and [https](http://nodejs.org/api/https.html) modules, but tries to provide an easier Domain Specific Language (DSL) for common kinds of Web Service interactions.
|
|
243
256
|
|
|
244
|
-
If you need to control options on http and https more directly, you pass a second
|
|
257
|
+
If you need to control options on `http` and `https` more directly, you pass a second parameter to `robot.http` that will be passed on to `ScopedHttpClient` which will be passed on to `http` and `https`:
|
|
245
258
|
|
|
246
|
-
```
|
|
247
|
-
options =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
robot.http(
|
|
259
|
+
```javascript
|
|
260
|
+
const options = {
|
|
261
|
+
rejectUnauthorized: false // don't verify server certificate against a CA, SCARY!
|
|
262
|
+
}
|
|
263
|
+
robot.http('https://midnight-train', options)
|
|
251
264
|
```
|
|
252
265
|
|
|
253
|
-
In addition, if
|
|
266
|
+
In addition, if `ScopedHttpClient` doesn't suit you, you can use [http](http://nodejs.org/api/http.html), [https](http://nodejs.org/api/https.html) or `fetch` directly.
|
|
254
267
|
|
|
255
268
|
## Random
|
|
256
269
|
|
|
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.
|
|
270
|
+
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. Hubot includes a convenience method:
|
|
258
271
|
|
|
259
|
-
```
|
|
260
|
-
lulz = ['lol', 'rofl', 'lmao']
|
|
261
|
-
|
|
262
|
-
res.send res.random lulz
|
|
272
|
+
```javascript
|
|
273
|
+
const lulz = ['lol', 'rofl', 'lmao']
|
|
274
|
+
res.send(res.random(lulz))
|
|
263
275
|
```
|
|
264
276
|
|
|
265
277
|
## Topic
|
|
266
278
|
|
|
267
279
|
Hubot can react to a room's topic changing, assuming that the adapter supports it.
|
|
268
280
|
|
|
269
|
-
```
|
|
270
|
-
module.exports = (robot)
|
|
271
|
-
robot.topic
|
|
272
|
-
res.send
|
|
281
|
+
```javascript
|
|
282
|
+
module.exports = (robot) => {
|
|
283
|
+
robot.topic((res) => {
|
|
284
|
+
res.send()`${res.message.text}? That's a Paddlin'`)
|
|
285
|
+
})
|
|
286
|
+
}
|
|
273
287
|
```
|
|
274
288
|
|
|
275
289
|
## Entering and leaving
|
|
276
290
|
|
|
277
291
|
Hubot can see users entering and leaving, assuming that the adapter supports it.
|
|
278
292
|
|
|
279
|
-
```
|
|
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
|
|
285
|
-
res.send
|
|
286
|
-
|
|
287
|
-
|
|
293
|
+
```javascript
|
|
294
|
+
const enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
|
|
295
|
+
const leaveReplies = ['Are you still there?', 'Target lost', 'Searching']
|
|
296
|
+
|
|
297
|
+
module.exports = (robot) => {
|
|
298
|
+
robot.enter(res) => {
|
|
299
|
+
res.send(res.random(enterReplies))
|
|
300
|
+
}
|
|
301
|
+
robot.leave(res) => {
|
|
302
|
+
res.send(res.random(leaveReplies))
|
|
303
|
+
}
|
|
304
|
+
}
|
|
288
305
|
```
|
|
289
306
|
|
|
290
307
|
## Custom Listeners
|
|
291
308
|
|
|
292
309
|
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
310
|
|
|
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
|
|
311
|
+
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 `res.match`.
|
|
295
312
|
|
|
296
|
-
```
|
|
297
|
-
module.exports = (robot)
|
|
313
|
+
```javascript
|
|
314
|
+
module.exports = (robot) =>{
|
|
298
315
|
robot.listen(
|
|
299
|
-
(message)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
316
|
+
(message) => {
|
|
317
|
+
// Match function
|
|
318
|
+
// only match messages with text (ie ignore enter and other events)
|
|
319
|
+
if(!message?.text) return
|
|
320
|
+
|
|
321
|
+
// Occassionally respond to things that Steve says
|
|
322
|
+
return message.user.name == 'Steve' && Math.random() > 0.8
|
|
323
|
+
},
|
|
324
|
+
(res) => {
|
|
325
|
+
// Standard listener callback
|
|
326
|
+
// Let Steve know how happy you are that he exists
|
|
327
|
+
res.reply(`HI STEVE! YOU'RE MY BEST FRIEND! (but only like ${res.match * 100}% of the time)`)
|
|
328
|
+
}
|
|
308
329
|
)
|
|
330
|
+
}
|
|
309
331
|
```
|
|
310
332
|
|
|
311
333
|
See [the design patterns document](patterns.md#dynamic-matching-of-messages) for examples of complex matchers.
|
|
312
334
|
|
|
313
335
|
## Environment variables
|
|
314
336
|
|
|
315
|
-
Hubot can access the environment he's running in, just like any other
|
|
337
|
+
Hubot can access the environment he's running in, just like any other Node.js 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
338
|
|
|
317
|
-
```
|
|
318
|
-
answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
|
339
|
+
```javascript
|
|
340
|
+
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
|
319
341
|
|
|
320
|
-
module.exports = (robot)
|
|
321
|
-
robot.respond
|
|
322
|
-
res.send
|
|
342
|
+
module.exports = (robot) => {
|
|
343
|
+
robot.respond(/what is the answer to the ultimate question of life/, (res) => {
|
|
344
|
+
res.send(`${answer}, but what is the question?`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
323
347
|
```
|
|
324
348
|
|
|
325
349
|
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
350
|
|
|
327
351
|
Here we can default to something:
|
|
328
352
|
|
|
329
|
-
```
|
|
330
|
-
answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
|
353
|
+
```javascript
|
|
354
|
+
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING ?? 42
|
|
331
355
|
|
|
332
|
-
module.exports = (robot)
|
|
333
|
-
robot.respond
|
|
334
|
-
res.send
|
|
356
|
+
module.exports = (robot) => {
|
|
357
|
+
robot.respond(/what is the answer to the ultimate question of life/, (res) => {
|
|
358
|
+
res.send(`${answer}, but what is the question?`)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
335
361
|
```
|
|
336
362
|
|
|
337
363
|
Here we exit if it's not defined:
|
|
338
364
|
|
|
339
|
-
```
|
|
340
|
-
answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
|
341
|
-
|
|
342
|
-
console.log
|
|
365
|
+
```javascript
|
|
366
|
+
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
|
367
|
+
if(!answer) {
|
|
368
|
+
console.log(`Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again`)
|
|
343
369
|
process.exit(1)
|
|
370
|
+
}
|
|
344
371
|
|
|
345
|
-
module.exports = (robot)
|
|
346
|
-
robot.respond
|
|
347
|
-
res.send
|
|
372
|
+
module.exports = (robot) => {
|
|
373
|
+
robot.respond(/what is the answer to the ultimate question of life/, (res) => {
|
|
374
|
+
res.send(`${answer}, but what is the question?`)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
348
377
|
```
|
|
349
378
|
|
|
350
379
|
And lastly, we update the `robot.respond` to check it:
|
|
351
380
|
|
|
352
|
-
```
|
|
353
|
-
answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
|
381
|
+
```javascript
|
|
382
|
+
const answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
|
|
354
383
|
|
|
355
|
-
module.exports = (robot)
|
|
356
|
-
robot.respond
|
|
357
|
-
|
|
358
|
-
res.send
|
|
359
|
-
|
|
360
|
-
res.send
|
|
384
|
+
module.exports = (robot) => {
|
|
385
|
+
robot.respond(/what is the answer to the ultimate question of life/, (res) => {
|
|
386
|
+
if(!answer) {
|
|
387
|
+
return res.send('Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again')
|
|
388
|
+
}
|
|
389
|
+
res.send(`${answer}, but what is the question?`)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
361
392
|
```
|
|
362
393
|
|
|
363
394
|
## Dependencies
|
|
@@ -366,7 +397,7 @@ Hubot uses [npm](https://github.com/isaacs/npm) to manage its dependencies. To a
|
|
|
366
397
|
|
|
367
398
|
```json
|
|
368
399
|
"dependencies": {
|
|
369
|
-
"hubot":
|
|
400
|
+
"hubot": "2.5.5",
|
|
370
401
|
"lolimadeupthispackage": "1.2.3"
|
|
371
402
|
},
|
|
372
403
|
```
|
|
@@ -377,48 +408,56 @@ If you are using scripts from hubot-scripts, take note of the `Dependencies` doc
|
|
|
377
408
|
|
|
378
409
|
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
410
|
|
|
380
|
-
```
|
|
381
|
-
module.exports = (robot)
|
|
382
|
-
robot.respond
|
|
383
|
-
setTimeout
|
|
384
|
-
res.send
|
|
385
|
-
, 60 * 1000
|
|
411
|
+
```javascript
|
|
412
|
+
module.exports = (robot) => {
|
|
413
|
+
robot.respond(/you are a little slow/, (res) => {
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
res.send(`Who you calling 'slow'?`)
|
|
416
|
+
}, 60 * 1000)
|
|
417
|
+
})
|
|
418
|
+
}
|
|
386
419
|
```
|
|
387
420
|
|
|
388
421
|
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
422
|
|
|
390
|
-
```
|
|
391
|
-
module.exports = (robot)
|
|
392
|
-
robot.respond
|
|
393
|
-
res.send
|
|
394
|
-
setInterval
|
|
395
|
-
res.send
|
|
396
|
-
, 1000
|
|
423
|
+
```javascript
|
|
424
|
+
module.exports = (robot) => {
|
|
425
|
+
robot.respond(/annoy me/, (res) => {
|
|
426
|
+
res.send('Hey, want to hear the most annoying sound in the world?')
|
|
427
|
+
setInterval(() => {
|
|
428
|
+
res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')
|
|
429
|
+
}, 1000)
|
|
430
|
+
})
|
|
431
|
+
}
|
|
397
432
|
```
|
|
398
433
|
|
|
399
434
|
Both `setTimeout` and `setInterval` return the ID of the timeout or interval it created. This can be used to to `clearTimeout` and `clearInterval`.
|
|
400
435
|
|
|
401
|
-
```
|
|
402
|
-
module.exports = (robot)
|
|
403
|
-
annoyIntervalId = null
|
|
436
|
+
```javascript
|
|
437
|
+
module.exports = (robot) => {
|
|
438
|
+
let annoyIntervalId = null
|
|
404
439
|
|
|
405
|
-
robot.respond
|
|
406
|
-
if annoyIntervalId
|
|
407
|
-
res.send
|
|
408
|
-
|
|
440
|
+
robot.respond(/annoy me/, (res) => {
|
|
441
|
+
if (annoyIntervalId) {
|
|
442
|
+
return res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')
|
|
443
|
+
}
|
|
409
444
|
|
|
410
|
-
res.send
|
|
411
|
-
annoyIntervalId = setInterval
|
|
412
|
-
res.send
|
|
413
|
-
, 1000
|
|
445
|
+
res.send('Hey, want to hear the most annoying sound in the world?')
|
|
446
|
+
annoyIntervalId = setInterval(() => {
|
|
447
|
+
res.send('AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH')
|
|
448
|
+
}, 1000)
|
|
449
|
+
}
|
|
414
450
|
|
|
415
|
-
robot.respond
|
|
416
|
-
if annoyIntervalId
|
|
417
|
-
res.send
|
|
451
|
+
robot.respond(/unannoy me/, (res) => {
|
|
452
|
+
if (annoyIntervalId) {
|
|
453
|
+
res.send('GUYS, GUYS, GUYS!')
|
|
418
454
|
clearInterval(annoyIntervalId)
|
|
419
455
|
annoyIntervalId = null
|
|
420
|
-
else
|
|
421
|
-
res.send
|
|
456
|
+
} else {
|
|
457
|
+
res.send('Not annoying you right now, am I?')
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
422
461
|
```
|
|
423
462
|
|
|
424
463
|
## HTTP Listener
|
|
@@ -430,25 +469,28 @@ You can increase the [maximum request body size](https://github.com/expressjs/bo
|
|
|
430
469
|
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
470
|
|
|
432
471
|
|
|
433
|
-
```
|
|
434
|
-
module.exports = (robot)
|
|
435
|
-
|
|
436
|
-
robot.router.post
|
|
437
|
-
room
|
|
438
|
-
data
|
|
439
|
-
secret = data.secret
|
|
472
|
+
```javascript
|
|
473
|
+
module.exports = (robot) => {
|
|
474
|
+
// the expected value of :room is going to vary by adapter, it might be a numeric id, name, token, or some other value
|
|
475
|
+
robot.router.post('/hubot/chatsecrets/:room', (req, res) => {
|
|
476
|
+
const room = req.params.room
|
|
477
|
+
const data = req.body?.payload ? JSON.parse(req.body.payload) : req.body
|
|
478
|
+
const secret = data.secret
|
|
440
479
|
|
|
441
|
-
robot.messageRoom
|
|
480
|
+
robot.messageRoom(room, `I have a secret: ${secret}`)
|
|
442
481
|
|
|
443
|
-
|
|
482
|
+
res.send('OK')
|
|
483
|
+
})
|
|
484
|
+
}
|
|
444
485
|
```
|
|
445
486
|
|
|
446
487
|
Test it with curl; also see section on [error handling](#error-handling) below.
|
|
447
|
-
|
|
448
|
-
|
|
488
|
+
|
|
489
|
+
```sh
|
|
490
|
+
# raw json, must specify Content-Type: application/json
|
|
449
491
|
curl -X POST -H "Content-Type: application/json" -d '{"secret":"C-TECH Astronomy"}' http://127.0.0.1:8080/hubot/chatsecrets/general
|
|
450
492
|
|
|
451
|
-
|
|
493
|
+
# defaults Content-Type: application/x-www-form-urlencoded, must st payload=...
|
|
452
494
|
curl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general
|
|
453
495
|
```
|
|
454
496
|
|
|
@@ -456,27 +498,31 @@ All endpoint URLs should start with the literal string `/hubot` (regardless of w
|
|
|
456
498
|
|
|
457
499
|
## Events
|
|
458
500
|
|
|
459
|
-
Hubot can also respond to events which can be used to pass data between scripts. This is done by encapsulating
|
|
501
|
+
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
502
|
|
|
461
503
|
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
504
|
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
module.exports = (robot)
|
|
466
|
-
robot.router.post
|
|
467
|
-
robot.emit
|
|
468
|
-
user
|
|
469
|
-
repo
|
|
470
|
-
hash
|
|
471
|
-
}
|
|
505
|
+
```javascript
|
|
506
|
+
// src/scripts/github-commits.js
|
|
507
|
+
module.exports = (robot) => {
|
|
508
|
+
robot.router.post('/hubot/gh-commits', (req, res) => {
|
|
509
|
+
robot.emit('commit', {
|
|
510
|
+
user: {}, //hubot user object
|
|
511
|
+
repo: 'https://github.com/github/hubot',
|
|
512
|
+
hash: '2e1951c089bd865839328592ff673d2f08153643'
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
}
|
|
472
516
|
```
|
|
473
517
|
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
module.exports = (robot)
|
|
477
|
-
robot.on
|
|
478
|
-
robot.send
|
|
479
|
-
|
|
518
|
+
```javascript
|
|
519
|
+
// src/scripts/heroku.js
|
|
520
|
+
module.exports = (robot) => {
|
|
521
|
+
robot.on('commit', (commit) => {
|
|
522
|
+
robot.send(commit.user, `Will now deploy ${commit.hash} from ${commit.repo}!`)
|
|
523
|
+
// deploy code goes here
|
|
524
|
+
}
|
|
525
|
+
}
|
|
480
526
|
```
|
|
481
527
|
|
|
482
528
|
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.
|
|
@@ -485,42 +531,49 @@ If you provide an event, it's highly recommended to include a hubot user or room
|
|
|
485
531
|
|
|
486
532
|
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
533
|
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
module.exports = (robot)
|
|
491
|
-
robot.error
|
|
492
|
-
robot.logger.error
|
|
534
|
+
```javascript
|
|
535
|
+
// src/scripts/does-not-compute.js
|
|
536
|
+
module.exports = (robot) => {
|
|
537
|
+
robot.error((err, res) => {
|
|
538
|
+
robot.logger.error('DOES NOT COMPUTE')
|
|
493
539
|
|
|
494
|
-
if
|
|
495
|
-
res.reply
|
|
540
|
+
if(res) {
|
|
541
|
+
res.reply('DOES NOT COMPUTE')
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
496
545
|
```
|
|
497
546
|
|
|
498
547
|
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
548
|
|
|
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
|
|
549
|
+
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 parameter is the error emitted, and the second parameter is an optional message that generated the error.
|
|
501
550
|
|
|
502
551
|
Using previous examples:
|
|
503
552
|
|
|
504
|
-
```
|
|
505
|
-
robot.router.post
|
|
506
|
-
room =
|
|
507
|
-
data = null
|
|
508
|
-
try
|
|
509
|
-
data = JSON.parse
|
|
510
|
-
catch
|
|
511
|
-
robot.emit
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
553
|
+
```javascript
|
|
554
|
+
robot.router.post()'/hubot/chatsecrets/:room', (req, res) => {
|
|
555
|
+
const room = req.params.room
|
|
556
|
+
let data = null
|
|
557
|
+
try {
|
|
558
|
+
data = JSON.parse(req.body.payload)
|
|
559
|
+
} catch(err) {
|
|
560
|
+
robot.emit('error', err)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// rest of the code here
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
robot.hear(/midnight train/i, (res) => {
|
|
567
|
+
robot.http('https://midnight-train')
|
|
568
|
+
.get()((err, res, body) => {
|
|
569
|
+
if (err) {
|
|
570
|
+
res.reply('Had problems taking the midnight train')
|
|
571
|
+
robot.emit('error', err, res)
|
|
522
572
|
return
|
|
523
|
-
|
|
573
|
+
}
|
|
574
|
+
// rest of code here
|
|
575
|
+
})
|
|
576
|
+
})
|
|
524
577
|
```
|
|
525
578
|
|
|
526
579
|
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.
|
|
@@ -529,113 +582,122 @@ For the second example, it's worth thinking about what messages the user would s
|
|
|
529
582
|
|
|
530
583
|
Hubot scripts can be documented with comments at the top of their file, for example:
|
|
531
584
|
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
585
|
+
```javascript
|
|
586
|
+
// Description:
|
|
587
|
+
// <description of the scripts functionality>
|
|
588
|
+
//
|
|
589
|
+
// Dependencies:
|
|
590
|
+
// "<module name>": "<module version>"
|
|
591
|
+
//
|
|
592
|
+
// Configuration:
|
|
593
|
+
// LIST_OF_ENV_VARS_TO_SET
|
|
594
|
+
//
|
|
595
|
+
// Commands:
|
|
596
|
+
// hubot <trigger> - <what the respond trigger does>
|
|
597
|
+
// <trigger> - <what the hear trigger does>
|
|
598
|
+
//
|
|
599
|
+
// Notes:
|
|
600
|
+
// <optional notes required for the script>
|
|
601
|
+
//
|
|
602
|
+
// Author:
|
|
603
|
+
// <github username of the original script author>
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
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 [hubot-help](https://github.com/hubotio/hubot-help) script 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
607
|
|
|
555
608
|
When documenting commands, here are some best practices:
|
|
556
609
|
|
|
557
610
|
* 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
611
|
* 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
612
|
* 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
|
|
613
|
+
* Check out how man pages document themselves. In particular, brackets indicate optional parts, '...' for any number of parameters, etc.
|
|
561
614
|
|
|
562
615
|
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
616
|
|
|
564
|
-
|
|
565
|
-
|
|
566
617
|
## Persistence
|
|
567
618
|
|
|
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`
|
|
619
|
+
Hubot has two persistence methods available that can be 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
620
|
|
|
571
621
|
### Brain
|
|
572
622
|
|
|
573
|
-
```
|
|
574
|
-
robot.respond
|
|
575
|
-
|
|
576
|
-
sodasHad = robot.brain.get('totalSodas') * 1
|
|
577
|
-
|
|
578
|
-
if sodasHad > 4
|
|
579
|
-
res.reply
|
|
580
|
-
else
|
|
581
|
-
res.reply
|
|
582
|
-
robot.brain.set
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
623
|
+
```javascript
|
|
624
|
+
robot.respond(/have a soda/i, (res) => {
|
|
625
|
+
// Get number of sodas had (coerced to a number).
|
|
626
|
+
const sodasHad = robot.brain.get('totalSodas') * 1 ?? 0
|
|
627
|
+
|
|
628
|
+
if (sodasHad > 4) {
|
|
629
|
+
res.reply(`I'm too fizzy..`)
|
|
630
|
+
} else {
|
|
631
|
+
res.reply('Sure!')
|
|
632
|
+
robot.brain.set('totalSodas', sodasHad + 1)
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
robot.respond(/sleep it off/i, (res) => {
|
|
637
|
+
robot.brain.set('totalSodas', 0)
|
|
638
|
+
res.reply('zzzzz')
|
|
639
|
+
}
|
|
587
640
|
```
|
|
588
641
|
|
|
589
642
|
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
643
|
|
|
591
|
-
```
|
|
592
|
-
module.exports = (robot)
|
|
593
|
-
|
|
594
|
-
|
|
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..
|
|
644
|
+
```javascript
|
|
645
|
+
module.exports = (robot) => {
|
|
646
|
+
robot.respond(/who is @?([\w .\-]+)\?*$/i, (res) => {
|
|
647
|
+
const name = res.match[1].trim()
|
|
601
648
|
|
|
602
|
-
|
|
649
|
+
const users = robot.brain.usersForFuzzyName(name)
|
|
650
|
+
if (users.length == 1) {
|
|
651
|
+
const user = users[0]
|
|
652
|
+
// Do something interesting here..
|
|
653
|
+
}
|
|
654
|
+
res.send(`${name} is user - ${user}`)
|
|
655
|
+
})
|
|
656
|
+
}
|
|
603
657
|
```
|
|
604
658
|
|
|
605
659
|
### Datastore
|
|
606
660
|
|
|
607
661
|
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
662
|
|
|
609
|
-
```
|
|
610
|
-
robot.respond
|
|
611
|
-
|
|
612
|
-
robot.datastore.get('totalSodas').then
|
|
613
|
-
sodasHad = value * 1
|
|
614
|
-
|
|
615
|
-
if sodasHad > 4
|
|
616
|
-
res.reply
|
|
617
|
-
else
|
|
618
|
-
res.reply
|
|
619
|
-
robot.brain.set
|
|
663
|
+
```javascript
|
|
664
|
+
robot.respond(/have a soda/i, (res) => {
|
|
665
|
+
// Get number of sodas had (coerced to a number).
|
|
666
|
+
robot.datastore.get('totalSodas').then((value) => {
|
|
667
|
+
const sodasHad = value * 1 ?? 0
|
|
668
|
+
|
|
669
|
+
if (sodasHad > 4) {
|
|
670
|
+
res.reply(`I'm too fizzy..`)
|
|
671
|
+
} else {
|
|
672
|
+
res.reply('Sure!')
|
|
673
|
+
robot.brain.set('totalSodas', sodasHad + 1)
|
|
674
|
+
}
|
|
675
|
+
})
|
|
676
|
+
})
|
|
620
677
|
|
|
621
|
-
robot.respond
|
|
622
|
-
robot.datastore.set('totalSodas', 0).then
|
|
623
|
-
res.reply
|
|
678
|
+
robot.respond(/sleep it off/i, (res) => {
|
|
679
|
+
robot.datastore.set('totalSodas', 0).then(() => {
|
|
680
|
+
res.reply('zzzzz')
|
|
681
|
+
})
|
|
682
|
+
})
|
|
624
683
|
```
|
|
625
684
|
|
|
626
685
|
The datastore also allows setting and getting values which are scoped to individual users:
|
|
627
686
|
|
|
628
|
-
```
|
|
687
|
+
```javascript
|
|
629
688
|
module.exports = (robot) ->
|
|
630
689
|
|
|
631
|
-
robot.respond
|
|
632
|
-
name = res.match[1].trim()
|
|
690
|
+
robot.respond(/who is @?([\w .\-]+)\?*$/i, (res) => {
|
|
691
|
+
const name = res.match[1].trim()
|
|
633
692
|
|
|
634
|
-
users = robot.brain.usersForFuzzyName(name)
|
|
635
|
-
if users.length
|
|
636
|
-
user = users[0]
|
|
637
|
-
user.get('roles').then
|
|
693
|
+
const users = robot.brain.usersForFuzzyName(name)
|
|
694
|
+
if (users.length == 1) {
|
|
695
|
+
const user = users[0]
|
|
696
|
+
user.get('roles').then((roles) => {
|
|
638
697
|
res.send "#{name} is #{roles.join(', ')}"
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
})
|
|
639
701
|
```
|
|
640
702
|
|
|
641
703
|
## Script Loading
|
|
@@ -648,9 +710,9 @@ There are three main sources to load scripts from:
|
|
|
648
710
|
|
|
649
711
|
Scripts loaded from the `scripts/` directory are loaded in alphabetical order, so you can expect a consistent load order of scripts. For example:
|
|
650
712
|
|
|
651
|
-
* `scripts/1-first.
|
|
652
|
-
* `scripts/_second.
|
|
653
|
-
* `scripts/third.
|
|
713
|
+
* `scripts/1-first.js`
|
|
714
|
+
* `scripts/_second.js`
|
|
715
|
+
* `scripts/third.js`
|
|
654
716
|
|
|
655
717
|
# Sharing Scripts
|
|
656
718
|
|
|
@@ -662,7 +724,7 @@ Start by [checking if an NPM package](index.md#scripts) for a script like yours
|
|
|
662
724
|
|
|
663
725
|
## Creating A Script Package
|
|
664
726
|
|
|
665
|
-
Creating a script package for hubot is very simple.
|
|
727
|
+
Creating a script package for hubot is very simple. Start by installing the `hubot` [yeoman](http://yeoman.io/) generator:
|
|
666
728
|
|
|
667
729
|
|
|
668
730
|
```
|
|
@@ -687,10 +749,10 @@ If you are using git, the generated directory includes a .gitignore, so you can
|
|
|
687
749
|
% git commit -m "Initial commit"
|
|
688
750
|
```
|
|
689
751
|
|
|
690
|
-
You now have a hubot script repository that's ready to roll! Feel free to crack open the pre-created `src/awesome-script.
|
|
752
|
+
You now have a hubot script repository that's ready to roll! Feel free to crack open the pre-created `src/awesome-script.js` 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
753
|
|
|
692
|
-
You'll probably want to write some unit tests for your new script.
|
|
693
|
-
`test/awesome-script-test.
|
|
754
|
+
You'll probably want to write some unit tests for your new script. A sample test script is written to
|
|
755
|
+
`test/awesome-script-test.js`, which you can run with `grunt`. For more information on tests,
|
|
694
756
|
see the [Testing Hubot Scripts](#testing-hubot-scripts) section.
|
|
695
757
|
|
|
696
758
|
# Listener Metadata
|
|
@@ -703,13 +765,16 @@ Additional extensions may define and handle additional metadata keys. For more i
|
|
|
703
765
|
|
|
704
766
|
Returning to an earlier example:
|
|
705
767
|
|
|
706
|
-
```
|
|
707
|
-
module.exports = (robot)
|
|
708
|
-
robot.respond
|
|
709
|
-
|
|
768
|
+
```javascript
|
|
769
|
+
module.exports = (robot) => {
|
|
770
|
+
robot.respond(/annoy me/, id:'annoyance.start', (res) => {
|
|
771
|
+
// code to annoy someone
|
|
772
|
+
})
|
|
710
773
|
|
|
711
|
-
robot.respond
|
|
712
|
-
|
|
774
|
+
robot.respond(/unannoy me/, id:'annoyance.stop', (res) => {
|
|
775
|
+
// code to stop annoying someone
|
|
776
|
+
})
|
|
777
|
+
}
|
|
713
778
|
```
|
|
714
779
|
|
|
715
780
|
These scoped identifiers allow you to externally specify new behaviors like:
|
|
@@ -734,10 +799,10 @@ Middleware is called with:
|
|
|
734
799
|
- See the each middleware type's API to see what the context will expose.
|
|
735
800
|
- `next`
|
|
736
801
|
- 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
|
|
802
|
+
- `next` should be called with a single, optional parameter: either the provided `done` function or a new function that eventually calls `done`. If the parameter is not given, the provided `done` will be assumed.
|
|
738
803
|
- `done`
|
|
739
804
|
- 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
|
|
805
|
+
- `done` should be called with no parameters
|
|
741
806
|
|
|
742
807
|
Every middleware receives the same API signature of `context`, `next`, and
|
|
743
808
|
`done`. Different kinds of middleware may receive different information in the
|
|
@@ -745,7 +810,7 @@ Every middleware receives the same API signature of `context`, `next`, and
|
|
|
745
810
|
|
|
746
811
|
### Error Handling
|
|
747
812
|
|
|
748
|
-
For synchronous middleware (never yields to the event loop), hubot will automatically catch errors and emit an
|
|
813
|
+
For synchronous middleware (never yields to the event loop), hubot will automatically catch errors and emit 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
814
|
|
|
750
815
|
# Listener Middleware
|
|
751
816
|
|
|
@@ -757,56 +822,65 @@ A fully functioning example can be found in [hubot-rate-limit](https://github.co
|
|
|
757
822
|
|
|
758
823
|
A simple example of middleware logging command executions:
|
|
759
824
|
|
|
760
|
-
```
|
|
761
|
-
module.exports = (robot)
|
|
762
|
-
robot.listenerMiddleware
|
|
763
|
-
|
|
764
|
-
robot.logger.info
|
|
765
|
-
|
|
825
|
+
```javascript
|
|
826
|
+
module.exports = (robot) => {
|
|
827
|
+
robot.listenerMiddleware((context, next, done) => {
|
|
828
|
+
// Log commands
|
|
829
|
+
robot.logger.info(`${context.response.message.user.name} asked me to ${context.response.message.text}`)
|
|
830
|
+
// Continue executing middleware
|
|
766
831
|
next()
|
|
832
|
+
})
|
|
833
|
+
}
|
|
767
834
|
```
|
|
768
835
|
|
|
769
836
|
In this example, a log message will be written for each chat message that matches a Listener.
|
|
770
837
|
|
|
771
838
|
A more complex example making a rate limiting decision:
|
|
772
839
|
|
|
773
|
-
```
|
|
774
|
-
module.exports = (robot)
|
|
775
|
-
|
|
776
|
-
lastExecutedTime = {}
|
|
777
|
-
|
|
778
|
-
robot.listenerMiddleware
|
|
779
|
-
try
|
|
780
|
-
|
|
781
|
-
minPeriodMs = context.listener.options?.rateLimits?.minPeriodMs
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if lastExecutedTime.hasOwnProperty(context.listener.options.id)
|
|
785
|
-
lastExecutedTime[context.listener.options.id] > Date.now() - minPeriodMs
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
else
|
|
789
|
-
next
|
|
840
|
+
```javascript
|
|
841
|
+
module.exports = (robot) => {
|
|
842
|
+
// Map of listener ID to last time it was executed
|
|
843
|
+
let lastExecutedTime = {}
|
|
844
|
+
|
|
845
|
+
robot.listenerMiddleware((context, next, done) => {
|
|
846
|
+
try {
|
|
847
|
+
// Default to 1s unless listener provides a different minimum period
|
|
848
|
+
const minPeriodMs = context.listener.options?.rateLimits?.minPeriodMs ?? 1000
|
|
849
|
+
|
|
850
|
+
// See if command has been executed recently
|
|
851
|
+
if (lastExecutedTime.hasOwnProperty(context.listener.options.id) &&
|
|
852
|
+
lastExecutedTime[context.listener.options.id] > Date.now() - minPeriodMs) {
|
|
853
|
+
// Command is being executed too quickly!
|
|
854
|
+
done()
|
|
855
|
+
} else {
|
|
856
|
+
next(()=> {
|
|
790
857
|
lastExecutedTime[context.listener.options.id] = Date.now()
|
|
791
858
|
done()
|
|
792
|
-
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
} catch(err) {
|
|
793
862
|
robot.emit('error', err, context.response)
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
}
|
|
794
866
|
```
|
|
795
867
|
|
|
796
868
|
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
869
|
|
|
798
870
|
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
871
|
|
|
800
|
-
```
|
|
801
|
-
module.exports = (robot)
|
|
802
|
-
robot.hear
|
|
803
|
-
|
|
804
|
-
res.reply
|
|
872
|
+
```javascript
|
|
873
|
+
module.exports = (robot) => {
|
|
874
|
+
robot.hear(/hello/, id: 'my-hello', rateLimits: {minPeriodMs: 10000}, (res) => {
|
|
875
|
+
// This will execute no faster than once every ten seconds
|
|
876
|
+
res.reply('Why, hello there!')
|
|
877
|
+
})
|
|
878
|
+
}
|
|
805
879
|
```
|
|
806
880
|
|
|
807
881
|
## Listener Middleware API
|
|
808
882
|
|
|
809
|
-
Listener middleware callbacks receive three
|
|
883
|
+
Listener middleware callbacks receive three parameters, `context`, `next`, and
|
|
810
884
|
`done`. See the [middleware API](#execution-process-and-api) for a description
|
|
811
885
|
of `next` and `done`. Listener middleware context includes these fields:
|
|
812
886
|
- `listener`
|
|
@@ -820,7 +894,7 @@ of `next` and `done`. Listener middleware context includes these fields:
|
|
|
820
894
|
# Receive Middleware
|
|
821
895
|
|
|
822
896
|
Receive middleware runs before any listeners have executed. It's suitable for
|
|
823
|
-
|
|
897
|
+
excluded commands that have not been updated to add an ID, metrics, and more.
|
|
824
898
|
|
|
825
899
|
## Receive Middleware Example
|
|
826
900
|
|
|
@@ -828,30 +902,33 @@ This simple middlware bans hubot use by a particular user, including `hear`
|
|
|
828
902
|
listeners. If the user attempts to run a command explicitly, it will return
|
|
829
903
|
an error message.
|
|
830
904
|
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
'12345'
|
|
905
|
+
```javascript
|
|
906
|
+
const EXCLUDED_USERS = [
|
|
907
|
+
'12345' // Restrict access for a user ID for a contractor
|
|
834
908
|
]
|
|
835
909
|
|
|
836
|
-
robot.receiveMiddleware
|
|
837
|
-
if context.response.message.user.id
|
|
838
|
-
|
|
910
|
+
robot.receiveMiddleware((context, next, done) => {
|
|
911
|
+
if (EXCLUDED_USERS.some( id => context.response.message.user.id == id)) {
|
|
912
|
+
// Don't process this message further.
|
|
839
913
|
context.response.message.finish()
|
|
840
914
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if context.response.message.text?.match(robot.respondPattern(''))
|
|
844
|
-
context.response.reply
|
|
915
|
+
// If the message starts with 'hubot' or the alias pattern, this user was
|
|
916
|
+
// explicitly trying to run a command, so respond with an error message.
|
|
917
|
+
if (context.response.message.text?.match(robot.respondPattern(''))) {
|
|
918
|
+
context.response.reply(`I'm sorry @${context.response.message.user.name}, but I'm configured to ignore your commands.`)
|
|
919
|
+
}
|
|
845
920
|
|
|
846
|
-
|
|
921
|
+
// Don't process further middleware.
|
|
847
922
|
done()
|
|
848
|
-
else
|
|
923
|
+
} else {
|
|
849
924
|
next(done)
|
|
925
|
+
}
|
|
926
|
+
})
|
|
850
927
|
```
|
|
851
928
|
|
|
852
929
|
## Receive Middleware API
|
|
853
930
|
|
|
854
|
-
Receive middleware callbacks receive three
|
|
931
|
+
Receive middleware callbacks receive three parameters, `context`, `next`, and
|
|
855
932
|
`done`. See the [middleware API](#execution-process-and-api) for a description
|
|
856
933
|
of `next` and `done`. Receive middleware context includes these fields:
|
|
857
934
|
- `response`
|
|
@@ -870,17 +947,21 @@ This simple example changes the format of links sent to a chat room from
|
|
|
870
947
|
markdown links (like [example](https://example.com)) to the format supported
|
|
871
948
|
by [Slack](https://slack.com), <https://example.com|example>.
|
|
872
949
|
|
|
873
|
-
```
|
|
874
|
-
module.exports = (robot)
|
|
875
|
-
robot.responseMiddleware
|
|
876
|
-
|
|
877
|
-
context.strings
|
|
950
|
+
```javascript
|
|
951
|
+
module.exports = (robot)=> {
|
|
952
|
+
robot.responseMiddleware((context, next, done) => {
|
|
953
|
+
if(!context.plaintext) return
|
|
954
|
+
context.strings.forEach(string => {
|
|
955
|
+
string.replace(/\[([^\[\]]*?)\]\((https?:\/\/.*?)\)/, "<$2|$1>"
|
|
956
|
+
})
|
|
878
957
|
next()
|
|
958
|
+
})
|
|
959
|
+
}
|
|
879
960
|
```
|
|
880
961
|
|
|
881
962
|
## Response Middleware API
|
|
882
963
|
|
|
883
|
-
Response middleware callbacks receive three
|
|
964
|
+
Response middleware callbacks receive three parameters, `context`, `next`, and
|
|
884
965
|
`done`. See the [middleware API](#execution-process-and-api) for a description
|
|
885
966
|
of `next` and `done`. Receive middleware context includes these fields:
|
|
886
967
|
- `response`
|
|
@@ -895,8 +976,8 @@ of `next` and `done`. Receive middleware context includes these fields:
|
|
|
895
976
|
# Testing Hubot Scripts
|
|
896
977
|
|
|
897
978
|
[hubot-test-helper](https://github.com/mtsmfm/hubot-test-helper) is a good
|
|
898
|
-
framework for unit testing Hubot scripts.
|
|
899
|
-
hubot-test-helper, you'll need a recent Node version with support for Promises.)
|
|
979
|
+
framework for unit testing Hubot scripts. (Note that, in order to use
|
|
980
|
+
hubot-test-helper, you'll need a recent Node.js version with support for Promises.)
|
|
900
981
|
|
|
901
982
|
Install the package in your Hubot instance:
|
|
902
983
|
|
|
@@ -909,10 +990,11 @@ You'll also need to install:
|
|
|
909
990
|
|
|
910
991
|
You may also want to install:
|
|
911
992
|
|
|
912
|
-
* *coffeescript* (if you're writing your tests in CoffeeScript rather than JavaScript)
|
|
913
993
|
* a mocking library such as *Sinon.js* (if your script performs webservice calls or
|
|
914
994
|
other asynchronous actions)
|
|
915
995
|
|
|
996
|
+
[Note: This section is still refering to Coffeescript, but we've update Hubot for Javascript. We'll have to replace this when we get a JavaScript example.]
|
|
997
|
+
|
|
916
998
|
Here is a sample script that tests the first couple of commands in the
|
|
917
999
|
[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
1000
|
|