hubot 3.3.2 → 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/nodejs-macos.yml +26 -0
- package/.github/workflows/nodejs-ubuntu.yml +28 -0
- package/.github/workflows/nodejs-windows.yml +26 -0
- package/.github/workflows/release.yml +36 -0
- package/.node-version +1 -0
- package/README.md +4 -3
- package/bin/hubot +1 -1
- package/bin/hubot.js +1 -5
- package/docs/adapters/development.md +35 -36
- package/docs/deploying/windows.md +5 -5
- package/docs/implementation.md +1 -3
- package/docs/index.md +5 -10
- package/docs/patterns.md +146 -101
- package/docs/scripting.md +493 -411
- package/es2015.js +1 -1
- package/package.json +22 -21
- package/src/adapters/campfire.js +18 -18
- package/src/adapters/shell.js +11 -14
- package/src/brain.js +8 -8
- package/src/datastore.js +3 -3
- package/src/httpclient.js +312 -0
- package/src/robot.js +19 -16
- package/src/user.js +2 -2
- package/test/adapter_test.js +1 -1
- package/test/brain_test.js +9 -9
- package/test/datastore_test.js +20 -16
- package/test/es2015_test.js +1 -1
- package/test/fixtures/mock-adapter.js +5 -0
- package/test/listener_test.js +2 -2
- package/test/middleware_test.js +18 -17
- package/test/robot_test.js +28 -13
- package/test/shell_test.js +72 -0
- package/test/user_test.js +2 -2
- package/.travis.yml +0 -27
- package/ROADMAP.md +0 -37
package/docs/patterns.md
CHANGED
|
@@ -15,23 +15,23 @@ When you rename Hubot, he will no longer respond to his former name. In order to
|
|
|
15
15
|
|
|
16
16
|
Setting this up is very easy:
|
|
17
17
|
|
|
18
|
-
1. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `rename-hubot.
|
|
18
|
+
1. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `rename-hubot.js`
|
|
19
19
|
2. Add the following code, modified for your needs:
|
|
20
20
|
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
# Commands:
|
|
26
|
-
# None
|
|
27
|
-
#
|
|
28
|
-
module.exports = (robot) ->
|
|
29
|
-
robot.hear /^hubot:? (.+)/i, (res) ->
|
|
30
|
-
response = "Sorry, I'm a diva and only respond to #{robot.name}"
|
|
31
|
-
response += " or #{robot.alias}" if robot.alias
|
|
32
|
-
res.reply response
|
|
33
|
-
return
|
|
21
|
+
```javascript
|
|
22
|
+
// Description:
|
|
23
|
+
// Tell people hubot's new name if they use the old one
|
|
34
24
|
|
|
25
|
+
// Commands:
|
|
26
|
+
// None
|
|
27
|
+
|
|
28
|
+
module.exports = (robot) => {
|
|
29
|
+
robot.hear(/^hubot:? (.+)/i, (res) => {
|
|
30
|
+
let response = `Sorry, I'm a diva and only respond to ${robot.name}`
|
|
31
|
+
response += robot.alias ? ` or ${robot.alias}` : ''
|
|
32
|
+
return res.reply(response)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
In the above pattern, modify both the hubot listener and the response message to suit your needs.
|
|
@@ -49,70 +49,72 @@ This pattern is similar to the Renaming the Hubot Instance pattern above:
|
|
|
49
49
|
|
|
50
50
|
Here is the setup:
|
|
51
51
|
|
|
52
|
-
1. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `deprecations.
|
|
52
|
+
1. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `deprecations.js`
|
|
53
53
|
2. Copy any old command listeners and add them to that file. For example, if you were to rename the help command for some silly reason:
|
|
54
54
|
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
module.exports = (robot)
|
|
63
|
-
robot.respond
|
|
64
|
-
res.reply
|
|
65
|
-
|
|
55
|
+
```javascript
|
|
56
|
+
// Description:
|
|
57
|
+
// Tell users when they have used commands that are deprecated or renamed
|
|
58
|
+
//
|
|
59
|
+
// Commands:
|
|
60
|
+
// None
|
|
61
|
+
//
|
|
62
|
+
module.exports = (robot) => {
|
|
63
|
+
robot.respond(/help\s*(.*)?$/i, (res) => {
|
|
64
|
+
return res.reply('That means nothing to me anymore. Perhaps you meant "docs" instead?')
|
|
65
|
+
})
|
|
66
|
+
}
|
|
66
67
|
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
## Preventing Hubot from Running Scripts Concurrently
|
|
70
71
|
|
|
71
|
-
Sometimes you have scripts that take several minutes to execute.
|
|
72
|
-
with by running subsequent commands, you may wish to code your scripts to prevent concurrent access.
|
|
72
|
+
Sometimes you have scripts that take several minutes to execute. If these scripts are doing something that could be interfered with by running subsequent commands, you may wish to code your scripts to prevent concurrent access.
|
|
73
73
|
|
|
74
|
-
To do this, you can set up a lock in the Hubot [brain](scripting.md#persistence) object.
|
|
75
|
-
can share the same lock if necessary.
|
|
74
|
+
To do this, you can set up a lock in the Hubot [brain](scripting.md#persistence) object. The lock is set up here so that different scripts can share the same lock if necessary.
|
|
76
75
|
|
|
77
76
|
Setting up the lock looks something like this:
|
|
78
77
|
|
|
79
|
-
```
|
|
80
|
-
module.exports = (robot)
|
|
81
|
-
robot.brain.on
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
```javascript
|
|
79
|
+
module.exports = (robot) => {
|
|
80
|
+
robot.brain.on('loaded', ()=>{
|
|
81
|
+
// Clear the lock on startup in case Hubot has restarted and Hubot's brain has persistence (e.g. redis).
|
|
82
|
+
// We don't want any orphaned locks preventing us from running commands.
|
|
84
83
|
robot.brain.remove('yourLockName')
|
|
84
|
+
}
|
|
85
85
|
|
|
86
|
-
robot.respond
|
|
87
|
-
lock = robot.brain.get('yourLockName')
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
86
|
+
robot.respond(/longrunningthing/i, (msg) => {
|
|
87
|
+
const lock = robot.brain.get('yourLockName')
|
|
88
|
+
if (lock) {
|
|
89
|
+
return msg.send(`I'm sorry, ${msg.message.user.name}, I'm afraid I can't do that. I'm busy doing something for ${lock.user.name}.`)
|
|
90
|
+
}
|
|
92
91
|
|
|
93
|
-
robot.brain.set('yourLockName', msg.message)
|
|
92
|
+
robot.brain.set('yourLockName', msg.message) // includes user, room, etc about who locked
|
|
94
93
|
|
|
95
|
-
yourLongClobberingAsyncThing
|
|
96
|
-
|
|
94
|
+
yourLongClobberingAsyncThing(err, res).then(
|
|
95
|
+
// Clear the lock
|
|
97
96
|
robot.brain.remove('yourLockName')
|
|
98
|
-
msg.reply
|
|
97
|
+
msg.reply('Finally Done')
|
|
98
|
+
)).catch(e => console.error(e))
|
|
99
|
+
}
|
|
99
100
|
```
|
|
100
101
|
|
|
101
102
|
## Forwarding all HTTP requests through a proxy
|
|
102
103
|
|
|
103
104
|
In many corporate environments, a web proxy is required to access the Internet and/or protected resources. For one-off control, use can specify an [Agent](https://nodejs.org/api/http.html) to use with `robot.http`. However, this would require modifying every script your robot uses to point at the proxy. Instead, you can specify the agent at the global level and have all HTTP requests use the agent by default.
|
|
104
105
|
|
|
105
|
-
Due to the way
|
|
106
|
+
Due to the way Node.js handles HTTP and HTTPS requests, you need to specify a different Agent for each protocol. ScopedHTTPClient will then automatically choose the right ProxyAgent for each request.
|
|
106
107
|
|
|
107
108
|
1. Install ProxyAgent. `npm install proxy-agent`
|
|
108
|
-
2. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `proxy.
|
|
109
|
+
2. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `proxy.js`
|
|
109
110
|
3. Add the following code, modified for your needs:
|
|
110
111
|
|
|
111
|
-
```
|
|
112
|
-
proxy = require
|
|
113
|
-
module.exports = (robot)
|
|
112
|
+
```javascript
|
|
113
|
+
const proxy = require('proxy-agent')
|
|
114
|
+
module.exports = (robot) => {
|
|
114
115
|
robot.globalHttpOptions.httpAgent = proxy('http://my-proxy-server.internal', false)
|
|
115
116
|
robot.globalHttpOptions.httpsAgent = proxy('http://my-proxy-server.internal', true)
|
|
117
|
+
}
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
## Dynamic matching of messages
|
|
@@ -123,27 +125,40 @@ In a simple robot, this isn't much different from just putting the conditions in
|
|
|
123
125
|
|
|
124
126
|
For example, the [factoid lookup command](https://github.com/github/hubot-scripts/blob/bd810f99f9394818a9dcc2ea3729427e4101b96d/src/scripts/factoid.coffee#L95-L99) could be reimplemented as:
|
|
125
127
|
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
```javascript
|
|
129
|
+
// use case: Hubot>fact1
|
|
130
|
+
// This listener doesn't require you to type the bot's name first
|
|
131
|
+
|
|
132
|
+
const {TextMessage} = require('../src/message')
|
|
133
|
+
module.exports = (robot) => {
|
|
134
|
+
// Dynamically populated list of factoids
|
|
135
|
+
const facts = {
|
|
136
|
+
fact1: 'stuff',
|
|
137
|
+
fact2: 'other stuff'
|
|
138
|
+
}
|
|
139
|
+
robot.listen(
|
|
140
|
+
// Matcher
|
|
141
|
+
(message) => {
|
|
142
|
+
// Check that message is a TextMessage type because
|
|
143
|
+
// if there is no match, this matcher function will
|
|
144
|
+
// be called again but the message type will be CatchAllMessage
|
|
145
|
+
// which doesn't have a `match` method.
|
|
146
|
+
if(!(message instanceof TextMessage)) return false
|
|
147
|
+
const match = message.match(/^(.*)$/)
|
|
148
|
+
// Only match if there is a matching factoid
|
|
149
|
+
if (match && match[1] in facts) {
|
|
150
|
+
return match[1]
|
|
151
|
+
} else {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
// Callback
|
|
156
|
+
(res) => {
|
|
157
|
+
const fact = res.match
|
|
158
|
+
res.reply(`${fact} is ${facts[fact]}`)
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
}
|
|
147
162
|
```
|
|
148
163
|
|
|
149
164
|
## Restricting access to commands
|
|
@@ -152,54 +167,69 @@ One of the awesome features of Hubot is its ability to make changes to a product
|
|
|
152
167
|
|
|
153
168
|
There are a variety of different patterns for restricting access that you can follow depending on your specific needs:
|
|
154
169
|
|
|
155
|
-
* Two buckets of access: full and restricted with
|
|
170
|
+
* Two buckets of access: full and restricted with include/exclude list
|
|
156
171
|
* Specific access rules for every command (Role-based Access Control)
|
|
157
|
-
*
|
|
172
|
+
* Include/exclude listing commands in specific rooms
|
|
158
173
|
|
|
159
174
|
### Simple per-listener access
|
|
160
175
|
|
|
161
176
|
In some organizations, almost all employees are given the same level of access and only a select few need to be restricted (e.g. new hires, contractors, etc.). In this model, you partition the set of all listeners to separate the "power commands" from the "normal commands".
|
|
162
177
|
|
|
163
|
-
Once you have segregated the listeners, you need to make some tradeoff decisions around
|
|
178
|
+
Once you have segregated the listeners, you need to make some tradeoff decisions around include/exclude users and listeners.
|
|
164
179
|
|
|
165
|
-
The key deciding factors for
|
|
166
|
-
|
|
167
|
-
*
|
|
180
|
+
The key deciding factors for inclusion vs exclusion of users are the number of users in each category, the frequency of change in either category, and the level of security risk your organization is willing to accept.
|
|
181
|
+
|
|
182
|
+
* Including users (users X, Y, Z have access to power commands; all other users only get access to normal commands) is a more secure method of access (new users have no default access to power commands), but has higher maintenance overhead (you need to add each new user to the "include" list).
|
|
183
|
+
* Excluding users (all users get access to power commands, except for users X, Y, Z, who only get access to normal commands) is a less secure method (new users have default access to power commands until they are added to the exclusion list), but has a much lower maintenance overhead if the exclusion list is small/rarely updated.
|
|
168
184
|
|
|
169
185
|
The key deciding factors for selectively allowing vs restricting listeners are the number of listeners in each category, the ratio of internal to external scripts, and the level of security risk your organization is willing to accept.
|
|
186
|
+
|
|
170
187
|
* Selectively allowing listeners (all listeners are power commands, except for listeners A, B, C, which are considered normal commands) is a more secure method (new listeners are restricted by default), but has a much higher maintenance overhead (every silly/fun listener needs to be explicity downgraded to "normal" status).
|
|
171
|
-
* Selectively restricting listeners (listeners A, B, C are power commands, everything else is a normal command) is a less secure method (new listeners are put into the normal category by default, which could give unexpected access; external scripts are particularly
|
|
188
|
+
* Selectively restricting listeners (listeners A, B, C are power commands, everything else is a normal command) is a less secure method (new listeners are put into the normal category by default, which could give unexpected access; external scripts are particularly risky here), but has a lower maintenance overhead (no need to modify/enumerate all the fun/culture scripts in your access policy).
|
|
172
189
|
|
|
173
190
|
As an additional consideration, most scripts do not currently have listener IDs, so you will likely need to open PRs (or fork) any external scripts you use to add listener IDs. The actual modification is easy, but coordinating with lots of maintainers can be time consuming.
|
|
174
191
|
|
|
175
192
|
Once you have decided which of the four possible models to follow, you need to build the appropriate lists of users and listeners to plug into your authorization middleware.
|
|
176
193
|
|
|
177
|
-
Example:
|
|
178
|
-
```coffeescript
|
|
179
|
-
POWER_COMMANDS = [
|
|
180
|
-
'deploy.web' # String that matches the listener ID
|
|
181
|
-
]
|
|
194
|
+
Example: inclusion list of users given access to selectively restricted power commands
|
|
182
195
|
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
```javascript
|
|
197
|
+
const POWER_COMMANDS = [
|
|
198
|
+
'deploy.web' // String that matches the listener ID
|
|
185
199
|
]
|
|
186
200
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
// Change name to something else to see it reject the command.
|
|
202
|
+
const POWER_USERS = [
|
|
203
|
+
'Shell' // String that matches the user ID set by the adapter
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
module.exports = (robot) => {
|
|
207
|
+
robot.listenerMiddleware((context, next, done) => {
|
|
208
|
+
if (POWER_COMMANDS.indexOf(context.listener.options.id) > -1) {
|
|
209
|
+
if (POWER_USERS.indexOf(context.response.message.user.name) > -1){
|
|
210
|
+
// User is allowed access to this command
|
|
211
|
+
next()
|
|
212
|
+
} else {
|
|
213
|
+
// Restricted command, but user isn't in whitelist
|
|
214
|
+
context.response.reply(`I'm sorry, @${context.response.message.user.name}, but you don't have access to do that.`)
|
|
215
|
+
done()
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
// This is not a restricted command; allow everyone
|
|
219
|
+
next()
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
robot.listen(message => {
|
|
224
|
+
return true
|
|
225
|
+
}, {id: 'deploy.web'},
|
|
226
|
+
res => {
|
|
227
|
+
res.reply('Deploying web...')
|
|
228
|
+
})
|
|
229
|
+
}
|
|
200
230
|
```
|
|
201
231
|
|
|
202
|
-
Remember that middleware executes for ALL listeners that match a given message (including `robot.hear
|
|
232
|
+
Remember that middleware executes for ALL listeners that match a given message (including `robot.hear(/.+/)`), so make sure you include them when categorizing your listeners.
|
|
203
233
|
|
|
204
234
|
### Specific access rules per listener
|
|
205
235
|
|
|
@@ -210,11 +240,26 @@ Example access policy:
|
|
|
210
240
|
* The Operations group has access to deploy all services (but not cut releases)
|
|
211
241
|
* The front desk cannot cut releases nor deploy services
|
|
212
242
|
|
|
213
|
-
Complex policies like this are currently best implemented in code directly
|
|
243
|
+
Complex policies like this are currently best implemented in code directly.
|
|
214
244
|
|
|
215
245
|
### Specific access rules per room
|
|
216
246
|
|
|
217
247
|
Organizations that have a number of chat rooms that serve different purposes often want to be able to use the same instance of hubot but have a different set of commands allowed in each room.
|
|
218
248
|
|
|
219
|
-
Work on generalized
|
|
249
|
+
Work on generalized exlusion list solution is [ongoing](https://github.com/kristenmills/hubot-command-blacklist). An inclusive list soultion could take a similar approach.
|
|
250
|
+
|
|
251
|
+
## Use scoped npm packages as adapter
|
|
220
252
|
|
|
253
|
+
It is possible to [install](https://docs.npmjs.com/cli/v7/commands/npm-install) package under a custom alias:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npm install <alias>@npm:<name>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
So for example to use `@foo/hubot-adapter` package as the adapter, you can:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npm install hubot-foo@npm:@foo/hubot-adapter
|
|
263
|
+
|
|
264
|
+
bin/hubot --adapter foo
|
|
265
|
+
```
|