UNPKG

12.9 kBMarkdownView Raw
1---
2permalink: /docs/patterns/
3---
4
5# Patterns
6
7Shared patterns for dealing with common Hubot scenarios.
8
9## Renaming the Hubot instance
10
11When you rename Hubot, he will no longer respond to his former name. In order to train your users on the new name, you may choose to add a deprecation notice when they try to say the old name. The pattern logic is:
12
13* listen to all messages that start with the old name
14* reply to the user letting them know about the new name
15
16Setting this up is very easy:
17
181. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `rename-hubot.js`
192. Add the following code, modified for your needs:
20
21```javascript
22// Description:
23// Tell people hubot's new name if they use the old one
24
25// Commands:
26// None
27
28module.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```
36
37In the above pattern, modify both the hubot listener and the response message to suit your needs.
38
39Also, it's important to note that the listener should be based on what hubot actually hears, instead of what is typed into the chat program before the Hubot Adapter has processed it. For example, the [HipChat Adapter](https://github.com/hipchat/hubot-hipchat) converts `@hubot` into `hubot:` before passing it to Hubot.
40
41## Deprecating or Renaming Listeners
42
43If you remove a script or change the commands for a script, it can be useful to let your users know about the change. One way is to just tell them in chat or let them discover the change by attempting to use a command that no longer exists. Another way is to have Hubot let people know when they've used a command that no longer works.
44
45This pattern is similar to the Renaming the Hubot Instance pattern above:
46
47* listen to all messages that match the old command
48* reply to the user letting them know that it's been deprecated
49
50Here is the setup:
51
521. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `deprecations.js`
532. 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
55```javascript
56// Description:
57// Tell users when they have used commands that are deprecated or renamed
58//
59// Commands:
60// None
61//
62module.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}
67
68```
69
70## Preventing Hubot from Running Scripts Concurrently
71
72Sometimes 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
74To 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.
75
76Setting up the lock looks something like this:
77
78```javascript
79module.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.
83 robot.brain.remove('yourLockName')
84 }
85
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 }
91
92 robot.brain.set('yourLockName', msg.message) // includes user, room, etc about who locked
93
94 yourLongClobberingAsyncThing(err, res).then(
95 // Clear the lock
96 robot.brain.remove('yourLockName')
97 msg.reply('Finally Done')
98 )).catch(e => console.error(e))
99}
100```
101
102## Forwarding all HTTP requests through a proxy
103
104In 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.
105
106Due 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.
107
1081. Install ProxyAgent. `npm install proxy-agent`
1092. Create a [bundled script](scripting.md) in the `scripts/` directory of your Hubot instance called `proxy.js`
1103. Add the following code, modified for your needs:
111
112```javascript
113const proxy = require('proxy-agent')
114module.exports = (robot) => {
115 robot.globalHttpOptions.httpAgent = proxy('http://my-proxy-server.internal', false)
116 robot.globalHttpOptions.httpsAgent = proxy('http://my-proxy-server.internal', true)
117}
118```
119
120## Dynamic matching of messages
121
122In some situations, you want to dynamically match different messages (e.g. factoids, JIRA projects). Rather than defining an overly broad regular expression that always matches, you can tell Hubot to only match when certain conditions are met.
123
124In a simple robot, this isn't much different from just putting the conditions in the Listener callback, but it makes a big difference when you are dealing with middleware: with the basic model, middleware will be executed for every match of the generic regex. With the dynamic matching model, middleware will only be executed when the dynamic conditions are matched.
125
126For example, the [factoid lookup command](https://github.com/github/hubot-scripts/blob/bd810f99f9394818a9dcc2ea3729427e4101b96d/src/scripts/factoid.coffee#L95-L99) could be reimplemented as:
127
128```javascript
129// use case: Hubot>fact1
130// This listener doesn't require you to type the bot's name first
131
132const {TextMessage} = require('../src/message')
133module.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}
162```
163
164## Restricting access to commands
165
166One of the awesome features of Hubot is its ability to make changes to a production environment with a single chat message. However, not everyone with access to your chat service should be able to trigger production changes.
167
168There are a variety of different patterns for restricting access that you can follow depending on your specific needs:
169
170* Two buckets of access: full and restricted with include/exclude list
171* Specific access rules for every command (Role-based Access Control)
172* Include/exclude listing commands in specific rooms
173
174### Simple per-listener access
175
176In 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".
177
178Once you have segregated the listeners, you need to make some tradeoff decisions around include/exclude users and listeners.
179
180The 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.
184
185The 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
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).
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).
189
190As 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.
191
192Once 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.
193
194Example: inclusion list of users given access to selectively restricted power commands
195
196```javascript
197const POWER_COMMANDS = [
198 'deploy.web' // String that matches the listener ID
199]
200
201// Change name to something else to see it reject the command.
202const POWER_USERS = [
203 'Shell' // String that matches the user ID set by the adapter
204]
205
206module.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}
230```
231
232Remember that middleware executes for ALL listeners that match a given message (including `robot.hear(/.+/)`), so make sure you include them when categorizing your listeners.
233
234### Specific access rules per listener
235
236For larger organizations, a binary categorization of access is usually insufficient and more complex access rules are required.
237
238Example access policy:
239* Each development team has access to cut releases and deploy their service
240* The Operations group has access to deploy all services (but not cut releases)
241* The front desk cannot cut releases nor deploy services
242
243Complex policies like this are currently best implemented in code directly.
244
245### Specific access rules per room
246
247Organizations 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.
248
249Work 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
252
253It is possible to [install](https://docs.npmjs.com/cli/v7/commands/npm-install) package under a custom alias:
254
255```bash
256npm install <alias>@npm:<name>
257```
258
259So for example to use `@foo/hubot-adapter` package as the adapter, you can:
260
261```bash
262npm install hubot-foo@npm:@foo/hubot-adapter
263
264bin/hubot --adapter foo
265```