hubot 8.0.3 → 9.0.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/bin/hubot.js CHANGED
@@ -153,5 +153,5 @@ function loadExternalScripts () {
153
153
 
154
154
  robot.adapter.once('connected', loadScripts)
155
155
 
156
- robot.run()
156
+ await robot.run()
157
157
  })()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hubot",
3
- "version": "8.0.3",
3
+ "version": "9.0.0",
4
4
  "author": "hubot",
5
5
  "keywords": [
6
6
  "github",
@@ -15,28 +15,21 @@
15
15
  "url": "https://github.com/hubotio/hubot.git"
16
16
  },
17
17
  "dependencies": {
18
- "async": "^3.2.4",
19
18
  "cline": "^0.8.2",
20
19
  "coffeescript": "^2.7.0",
21
20
  "connect-multiparty": "^2.2.0",
22
21
  "express": "^4.18.2",
23
22
  "express-basic-auth": "^1.2.1",
24
23
  "optparse": "^1.0.5",
25
- "pino": "^8.11.0",
26
- "standard": "^17.1.0"
24
+ "pino": "^8.11.0"
27
25
  },
28
26
  "devDependencies": {
29
- "chai": "^4.3.7",
30
- "is-circular": "^1.0.2",
31
- "mocha": "^10.2.0",
32
- "mockery": "^2.1.0",
33
27
  "semantic-release": "^21.0.1",
34
- "sinon": "^15.0.4",
35
- "sinon-chai": "^3.7.0"
28
+ "standard": "^17.1.0"
36
29
  },
37
30
  "engines": {
38
- "node": "> 4.0.0",
39
- "npm": "> 2.0.0"
31
+ "node": ">= 18",
32
+ "npm": ">= 9"
40
33
  },
41
34
  "main": "./index",
42
35
  "bin": {
@@ -45,7 +38,7 @@
45
38
  "scripts": {
46
39
  "start": "bin/hubot",
47
40
  "pretest": "standard",
48
- "test": "mocha --exit",
41
+ "test": "node --test test/*_test.js",
49
42
  "test:smoke": "node src/**/*.js",
50
43
  "test:e2e": "bin/e2e-test.sh"
51
44
  },
package/src/adapter.js CHANGED
@@ -16,8 +16,8 @@ class Adapter extends EventEmitter {
16
16
  // envelope - A Object with message, room and user details.
17
17
  // strings - One or more Strings for each message to send.
18
18
  //
19
- // Returns nothing.
20
- send (envelope/* , ...strings */) {}
19
+ // Returns results from adapter.
20
+ async send (envelope, ...strings) {}
21
21
 
22
22
  // Public: Raw method for sending emote data back to the chat source.
23
23
  // Defaults as an alias for send
@@ -25,10 +25,9 @@ class Adapter extends EventEmitter {
25
25
  // envelope - A Object with message, room and user details.
26
26
  // strings - One or more Strings for each message to send.
27
27
  //
28
- // Returns nothing.
29
- emote (envelope/* , ...strings */) {
30
- const strings = [].slice.call(arguments, 1)
31
- return this.send.apply(this, [envelope].concat(strings))
28
+ // Returns results from adapter.
29
+ async emote (envelope, ...strings) {
30
+ return this.send(envelope, ...strings)
32
31
  }
33
32
 
34
33
  // Public: Raw method for building a reply and sending it back to the chat
@@ -37,63 +36,68 @@ class Adapter extends EventEmitter {
37
36
  // envelope - A Object with message, room and user details.
38
37
  // strings - One or more Strings for each reply to send.
39
38
  //
40
- // Returns nothing.
41
- reply (envelope/* , ...strings */) {}
39
+ // Returns results from adapter.
40
+ async reply (envelope, ...strings) {}
42
41
 
43
42
  // Public: Raw method for setting a topic on the chat source. Extend this.
44
43
  //
45
44
  // envelope - A Object with message, room and user details.
46
45
  // strings - One more more Strings to set as the topic.
47
46
  //
48
- // Returns nothing.
49
- topic (envelope/* , ...strings */) {}
47
+ // Returns results from adapter.
48
+ async topic (envelope, ...strings) {}
50
49
 
51
50
  // Public: Raw method for playing a sound in the chat source. Extend this.
52
51
  //
53
52
  // envelope - A Object with message, room and user details.
54
53
  // strings - One or more strings for each play message to send.
55
54
  //
56
- // Returns nothing
57
- play (envelope/* , ...strings */) {}
55
+ // Returns results from adapter.
56
+ async play (envelope, ...strings) {}
58
57
 
59
58
  // Public: Raw method for invoking the bot to run. Extend this.
60
59
  //
61
- // Returns nothing.
62
- run () {}
60
+ // Returns whatever the extended adapter returns.
61
+ async run () {}
63
62
 
64
63
  // Public: Raw method for shutting the bot down. Extend this.
65
64
  //
66
65
  // Returns nothing.
67
- close () {}
66
+ close () {
67
+ this.removeAllListeners()
68
+ }
68
69
 
69
70
  // Public: Dispatch a received message to the robot.
70
71
  //
71
72
  // Returns nothing.
72
- receive (message) {
73
- this.robot.receive(message)
73
+ async receive (message) {
74
+ await this.robot.receive(message)
74
75
  }
75
76
 
76
77
  // Public: Get an Array of User objects stored in the brain.
77
78
  //
78
79
  // Returns an Array of User objects.
80
+ // @deprecated Use @robot.brain
79
81
  users () {
80
- this.robot.logger.warning('@users() is going to be deprecated in 3.0.0 use @robot.brain.users()')
82
+ this.robot.logger.warning('@users() is going to be deprecated in 11.0.0 use @robot.brain.users()')
81
83
  return this.robot.brain.users()
82
84
  }
83
85
 
84
86
  // Public: Get a User object given a unique identifier.
85
87
  //
86
88
  // Returns a User instance of the specified user.
89
+ // @deprecated Use @robot.brain
87
90
  userForId (id, options) {
88
- this.robot.logger.warning('@userForId() is going to be deprecated in 3.0.0 use @robot.brain.userForId()')
91
+ this.robot.logger.warning('@userForId() is going to be deprecated in 11.0.0 use @robot.brain.userForId()')
89
92
  return this.robot.brain.userForId(id, options)
90
93
  }
91
94
 
92
95
  // Public: Get a User object given a name.
93
96
  //
94
97
  // Returns a User instance for the user with the specified name.
98
+ // @deprecated Use @robot.brain
95
99
  userForName (name) {
96
- this.robot.logger.warning('@userForName() is going to be deprecated in 3.0.0 use @robot.brain.userForName()')
100
+ this.robot.logger.warning('@userForName() is going to be deprecated in 11.0.0 use @robot.brain.userForName()')
97
101
  return this.robot.brain.userForName(name)
98
102
  }
99
103
 
@@ -102,8 +106,9 @@ class Adapter extends EventEmitter {
102
106
  // nicknames, etc.
103
107
  //
104
108
  // Returns an Array of User instances matching the fuzzy name.
109
+ // @deprecated Use @robot.brain
105
110
  usersForRawFuzzyName (fuzzyName) {
106
- this.robot.logger.warning('@userForRawFuzzyName() is going to be deprecated in 3.0.0 use @robot.brain.userForRawFuzzyName()')
111
+ this.robot.logger.warning('@userForRawFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForRawFuzzyName()')
107
112
  return this.robot.brain.usersForRawFuzzyName(fuzzyName)
108
113
  }
109
114
 
@@ -112,8 +117,9 @@ class Adapter extends EventEmitter {
112
117
  // fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).
113
118
  //
114
119
  // Returns an Array of User instances matching the fuzzy name.
120
+ // @deprecated Use @robot.brain
115
121
  usersForFuzzyName (fuzzyName) {
116
- this.robot.logger.warning('@userForFuzzyName() is going to be deprecated in 3.0.0 use @robot.brain.userForFuzzyName()')
122
+ this.robot.logger.warning('@userForFuzzyName() is going to be deprecated in 11.0.0 use @robot.brain.userForFuzzyName()')
117
123
  return this.robot.brain.usersForFuzzyName(fuzzyName)
118
124
  }
119
125
 
@@ -123,8 +129,9 @@ class Adapter extends EventEmitter {
123
129
  // send the request.
124
130
  //
125
131
  // Returns a ScopedClient instance.
132
+ // @deprecated Use node.js fetch.
126
133
  http (url) {
127
- this.robot.logger.warning('@http() is going to be deprecated in 3.0.0 use @robot.http()')
134
+ this.robot.logger.warning('@http() is going to be deprecated in 11.0.0 use @robot.http()')
128
135
  return this.robot.http(url)
129
136
  }
130
137
  }
@@ -82,7 +82,7 @@ class Campfire extends Adapter {
82
82
  })
83
83
  }
84
84
 
85
- run () {
85
+ async run () {
86
86
  const self = this
87
87
 
88
88
  const options = {
@@ -17,49 +17,52 @@ const historyPath = '.hubot_history'
17
17
  const bold = str => `\x1b[1m${str}\x1b[22m`
18
18
 
19
19
  class Shell extends Adapter {
20
+ #rl = null
20
21
  constructor (robot) {
21
22
  super(robot)
22
23
  this.name = 'Shell'
23
24
  }
24
25
 
25
- send (envelope/* , ...strings */) {
26
- const strings = [].slice.call(arguments, 1)
27
-
26
+ async send (envelope, ...strings) {
28
27
  Array.from(strings).forEach(str => console.log(bold(str)))
29
28
  }
30
29
 
31
- emote (envelope/* , ...strings */) {
32
- const strings = [].slice.call(arguments, 1)
30
+ async emote (envelope, ...strings) {
33
31
  Array.from(strings).map(str => this.send(envelope, `* ${str}`))
34
32
  }
35
33
 
36
- reply (envelope/* , ...strings */) {
37
- const strings = [].slice.call(arguments, 1).map((s) => `${envelope.user.name}: ${s}`)
38
-
39
- this.send.apply(this, [envelope].concat(strings))
34
+ async reply (envelope, ...strings) {
35
+ strings = strings.map((s) => `${envelope.user.name}: ${s}`)
36
+ this.send(envelope, ...strings)
40
37
  }
41
38
 
42
- run () {
39
+ async run () {
43
40
  this.buildCli()
44
- loadHistory((error, history) => {
45
- if (error) {
46
- console.log(error.message)
47
- }
41
+ try {
42
+ const { readlineInterface, history } = await this.#loadHistory()
48
43
  this.cli.history(history)
49
- this.cli.interact(`${this.robot.name}> `)
50
- return this.emit('connected', this)
51
- })
44
+ this.cli.interact(`${this.robot.name ?? this.robot.alias}> `)
45
+ this.#rl = readlineInterface
46
+ this.emit('connected', this)
47
+ } catch (error) {
48
+ console.log(error)
49
+ }
52
50
  }
53
51
 
54
- shutdown () {
55
- this.robot.shutdown()
56
- return process.exit(0)
52
+ close () {
53
+ super.close()
54
+ // Getting an error message on GitHubt Actions: error: 'this[#rl].close is not a function'
55
+ if (this.#rl?.close) {
56
+ this.#rl.close()
57
+ }
58
+ this.cli.removeAllListeners()
59
+ this.cli.close()
57
60
  }
58
61
 
59
62
  buildCli () {
60
63
  this.cli = cline()
61
64
 
62
- this.cli.command('*', input => {
65
+ this.cli.command('*', async input => {
63
66
  let userId = process.env.HUBOT_SHELL_USER_ID || '1'
64
67
  if (userId.match(/A\d+z/)) {
65
68
  userId = parseInt(userId)
@@ -67,7 +70,7 @@ class Shell extends Adapter {
67
70
 
68
71
  const userName = process.env.HUBOT_SHELL_USER_NAME || 'Shell'
69
72
  const user = this.robot.brain.userForId(userId, { name: userName, room: 'Shell' })
70
- this.receive(new TextMessage(user, input, 'messageId'))
73
+ await this.receive(new TextMessage(user, input, 'messageId'))
71
74
  })
72
75
 
73
76
  this.cli.command('history', () => {
@@ -90,7 +93,7 @@ class Shell extends Adapter {
90
93
  history = this.cli.history()
91
94
 
92
95
  if (history.length <= historySize) {
93
- return this.shutdown()
96
+ return
94
97
  }
95
98
 
96
99
  const startIndex = history.length - historySize
@@ -100,11 +103,35 @@ class Shell extends Adapter {
100
103
  }
101
104
 
102
105
  const outstream = fs.createWriteStream(historyPath, fileOpts)
103
- outstream.on('end', this.shutdown.bind(this))
104
106
  for (i = 0, len = history.length; i < len; i++) {
105
107
  item = history[i]
106
108
  outstream.write(item + '\n')
107
109
  }
110
+ outstream.end()
111
+ })
112
+ }
113
+
114
+ async #loadHistory () {
115
+ if (!fs.existsSync(historyPath)) {
116
+ return new Error('No history available')
117
+ }
118
+ const instream = fs.createReadStream(historyPath)
119
+ const outstream = new Stream()
120
+ outstream.readable = true
121
+ outstream.writable = true
122
+ const history = []
123
+ const readlineInterface = readline.createInterface({ input: instream, output: outstream, terminal: false })
124
+ return new Promise((resolve, reject) => {
125
+ readlineInterface.on('line', line => {
126
+ line = line.trim()
127
+ if (line.length > 0) {
128
+ history.push(line)
129
+ }
130
+ })
131
+ readlineInterface.on('close', () => {
132
+ resolve({ readlineInterface, history })
133
+ })
134
+ readlineInterface.on('error', reject)
108
135
  })
109
136
  }
110
137
  }
@@ -112,29 +139,3 @@ class Shell extends Adapter {
112
139
  // Prevent output buffer "swallowing" every other character on OSX / Node version > 16.19.0.
113
140
  process.stdout._handle.setBlocking(false)
114
141
  exports.use = robot => new Shell(robot)
115
-
116
- // load history from .hubot_history.
117
- //
118
- // callback - A Function that is called with the loaded history items (or an empty array if there is no history)
119
- function loadHistory (callback) {
120
- if (!fs.existsSync(historyPath)) {
121
- return callback(new Error('No history available'))
122
- }
123
-
124
- const instream = fs.createReadStream(historyPath)
125
- const outstream = new Stream()
126
- outstream.readable = true
127
- outstream.writable = true
128
-
129
- const items = []
130
-
131
- readline.createInterface({ input: instream, output: outstream, terminal: false })
132
- .on('line', function (line) {
133
- line = line.trim()
134
- if (line.length > 0) {
135
- items.push(line)
136
- }
137
- })
138
- .on('close', () => callback(null, items))
139
- .on('error', callback)
140
- }
package/src/brain.js CHANGED
@@ -109,6 +109,7 @@ class Brain extends EventEmitter {
109
109
  clearInterval(this.saveInterval)
110
110
  this.save()
111
111
  this.emit('close')
112
+ this.removeAllListeners()
112
113
  }
113
114
 
114
115
  // Public: Enable or disable the automatic saving
package/src/datastore.js CHANGED
@@ -13,52 +13,49 @@ class DataStore {
13
13
  // write has completed.
14
14
  //
15
15
  // Value can be any JSON-serializable type.
16
- set (key, value) {
17
- return this._set(key, value, 'global')
16
+ async set (key, value) {
17
+ return await this._set(key, value, 'global')
18
18
  }
19
19
 
20
20
  // Public: Assuming `key` represents an object in the database,
21
21
  // sets its `objectKey` to `value`. If `key` isn't already
22
22
  // present, it's instantiated as an empty object.
23
- setObject (key, objectKey, value) {
24
- return this.get(key).then((object) => {
25
- const target = object || {}
26
- target[objectKey] = value
27
- return this.set(key, target)
28
- })
23
+ async setObject (key, objectKey, value) {
24
+ const object = await this.get(key)
25
+ const target = object || {}
26
+ target[objectKey] = value
27
+ return await this.set(key, target)
29
28
  }
30
29
 
31
30
  // Public: Adds the supplied value(s) to the end of the existing
32
31
  // array in the database marked by `key`. If `key` isn't already
33
32
  // present, it's instantiated as an empty array.
34
- setArray (key, value) {
35
- return this.get(key).then((object) => {
36
- const target = object || []
37
- // Extend the array if the value is also an array, otherwise
38
- // push the single value on the end.
39
- if (Array.isArray(value)) {
40
- return this.set(key, target.push.apply(target, value))
41
- } else {
42
- return this.set(key, target.concat(value))
43
- }
44
- })
33
+ async setArray (key, value) {
34
+ const object = await this.get(key)
35
+ const target = object ?? []
36
+ // Extend the array if the value is also an array, otherwise
37
+ // push the single value on the end.
38
+ if (Array.isArray(value)) {
39
+ return await this.set(key, target.concat(value))
40
+ } else {
41
+ return await this.set(key, target.concat([value]))
42
+ }
45
43
  }
46
44
 
47
45
  // Public: Get value by key if in the database or return `undefined`
48
46
  // if not found. Returns a promise which resolves to the
49
47
  // requested value.
50
- get (key) {
51
- return this._get(key, 'global')
48
+ async get (key) {
49
+ return await this._get(key, 'global')
52
50
  }
53
51
 
54
52
  // Public: Digs inside the object at `key` for a key named
55
53
  // `objectKey`. If `key` isn't already present, or if it doesn't
56
54
  // contain an `objectKey`, returns `undefined`.
57
- getObject (key, objectKey) {
58
- return this.get(key).then((object) => {
59
- const target = object || {}
60
- return target[objectKey]
61
- })
55
+ async getObject (key, objectKey) {
56
+ const object = await this.get(key)
57
+ const target = object || {}
58
+ return target[objectKey]
62
59
  }
63
60
 
64
61
  // Private: Implements the underlying `set` logic for the datastore.
@@ -70,7 +67,7 @@ class DataStore {
70
67
  // This returns a resolved promise when the `set` operation is
71
68
  // successful, and a rejected promise if the operation fails.
72
69
  _set (key, value, table) {
73
- return Promise.reject(new DataStoreUnavailable('Setter called on the abstract class.'))
70
+ throw new DataStoreUnavailable('Setter called on the abstract class.')
74
71
  }
75
72
 
76
73
  // Private: Implements the underlying `get` logic for the datastore.
@@ -82,7 +79,7 @@ class DataStore {
82
79
  // This returns a resolved promise containing the fetched value on
83
80
  // success, and a rejected promise if the operation fails.
84
81
  _get (key, table) {
85
- return Promise.reject(new DataStoreUnavailable('Getter called on the abstract class.'))
82
+ throw new DataStoreUnavailable('Getter called on the abstract class.')
86
83
  }
87
84
  }
88
85
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const DataStore = require('../datastore').DataStore
3
+ const DataStore = require('../datastore.js').DataStore
4
4
 
5
5
  class InMemoryDataStore extends DataStore {
6
6
  constructor (robot) {
@@ -11,11 +11,11 @@ class InMemoryDataStore extends DataStore {
11
11
  }
12
12
  }
13
13
 
14
- _get (key, table) {
14
+ async _get (key, table) {
15
15
  return Promise.resolve(this.data[table][key])
16
16
  }
17
17
 
18
- _set (key, value, table) {
18
+ async _set (key, value, table) {
19
19
  return Promise.resolve(this.data[table][key] = value)
20
20
  }
21
21
  }
package/src/listener.js CHANGED
@@ -20,19 +20,19 @@ class Listener {
20
20
  constructor (robot, matcher, options, callback) {
21
21
  this.robot = robot
22
22
  this.matcher = matcher
23
- this.options = options
23
+ this.options = options ?? {}
24
24
  this.callback = callback
25
25
 
26
26
  if (this.matcher == null) {
27
27
  throw new Error('Missing a matcher for Listener')
28
28
  }
29
29
 
30
- if (this.callback == null) {
30
+ if (!this.callback) {
31
31
  this.callback = this.options
32
32
  this.options = {}
33
33
  }
34
34
 
35
- if (this.options.id == null) {
35
+ if (!this.options?.id) {
36
36
  this.options.id = null
37
37
  }
38
38
 
@@ -49,60 +49,40 @@ class Listener {
49
49
  //
50
50
  // message - A Message instance.
51
51
  // middleware - Optional Middleware object to execute before the Listener callback
52
- // callback - Optional Function called with a boolean of whether the matcher matched
53
52
  //
54
- // Returns a boolean of whether the matcher matched.
55
- // Returns before executing callback
56
- call (message, middleware, didMatchCallback) {
57
- // middleware argument is optional
58
- if (didMatchCallback == null && typeof middleware === 'function') {
59
- didMatchCallback = middleware
60
- middleware = undefined
53
+ // Returns the result of the callback.
54
+ async call (message, middleware) {
55
+ if (middleware && typeof middleware === 'function') {
56
+ const fn = middleware
57
+ middleware = new Middleware(this.robot)
58
+ middleware.register(fn)
61
59
  }
62
60
 
63
- // ensure we have a Middleware object
64
- if (middleware == null) {
61
+ if (!middleware) {
65
62
  middleware = new Middleware(this.robot)
66
63
  }
67
64
 
68
65
  const match = this.matcher(message)
69
- if (match) {
70
- if (this.regex) {
71
- this.robot.logger.debug(`Message '${message}' matched regex /${inspect(this.regex)}/; listener.options = ${inspect(this.options)}`)
72
- }
73
-
74
- // special middleware-like function that always executes the Listener's
75
- // callback and calls done (never calls 'next')
76
- const executeListener = (context, done) => {
77
- this.robot.logger.debug(`Executing listener callback for Message '${message}'`)
78
- try {
79
- this.callback(context.response)
80
- } catch (err) {
81
- this.robot.emit('error', err, context.response)
82
- }
83
- done()
84
- }
66
+ if (!match) return null
67
+ if (this.regex) {
68
+ this.robot.logger.debug(`Message '${message}' matched regex /${inspect(this.regex)}/; listener.options = ${inspect(this.options)}`)
69
+ }
85
70
 
86
- // When everything is finished (down the middleware stack and back up),
87
- // pass control back to the robot
88
- const allDone = function allDone () {
89
- // Yes, we tried to execute the listener callback (middleware may
90
- // have intercepted before actually executing though)
91
- if (didMatchCallback != null) {
92
- process.nextTick(() => didMatchCallback(true))
93
- }
94
- }
71
+ const response = new this.robot.Response(this.robot, message, match)
95
72
 
96
- const response = new this.robot.Response(this.robot, message, match)
97
- middleware.execute({ listener: this, response }, executeListener, allDone)
98
- return true
99
- } else {
100
- if (didMatchCallback != null) {
101
- // No, we didn't try to execute the listener callback
102
- process.nextTick(() => didMatchCallback(false))
103
- }
104
- return false
73
+ try {
74
+ const shouldContinue = await middleware.execute({ listener: this, response })
75
+ if (shouldContinue === false) return null
76
+ } catch (e) {
77
+ this.robot.logger.error(`Error executing middleware for listener: ${e.stack}`)
78
+ }
79
+ try {
80
+ return await this.callback(response)
81
+ } catch (e) {
82
+ this.robot.logger.error(`Error executing listener callback: ${e.stack}`)
83
+ this.robot.emit('error', e, response)
105
84
  }
85
+ return null
106
86
  }
107
87
  }
108
88
 
package/src/message.js CHANGED
@@ -7,7 +7,7 @@ class Message {
7
7
  constructor (user, done) {
8
8
  this.user = user
9
9
  this.done = done || false
10
- this.room = this.user.room
10
+ this.room = this.user?.room
11
11
  }
12
12
 
13
13
  // Indicates that no other Listener should be called on this object
package/src/middleware.js CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const async = require('async')
4
-
5
3
  class Middleware {
6
4
  constructor (robot) {
7
5
  this.robot = robot
@@ -16,68 +14,29 @@ class Middleware {
16
14
  // context - context object that is passed through the middleware stack.
17
15
  // When handling errors, this is assumed to have a `response` property.
18
16
  //
19
- // next(context, done) - Called when all middleware is complete (assuming
20
- // all continued by calling respective 'next' functions)
21
- //
22
- // done() - Initial (final) completion callback. May be wrapped by
23
- // executed middleware.
24
- //
25
- // Returns nothing
26
- // Returns before executing any middleware
27
- execute (context, next, done) {
28
- const self = this
29
-
30
- if (done == null) {
31
- done = function () {}
32
- }
33
-
34
- // Execute a single piece of middleware and update the completion callback
35
- // (each piece of middleware can wrap the 'done' callback with additional
36
- // logic).
37
- function executeSingleMiddleware (doneFunc, middlewareFunc, cb) {
38
- // Match the async.reduce interface
39
- function nextFunc (newDoneFunc) {
40
- cb(null, newDoneFunc || doneFunc)
41
- }
42
-
43
- // Catch errors in synchronous middleware
17
+ // Returns bool, true | false, whether or not to continue execution
18
+ async execute (context) {
19
+ let shouldContinue = true
20
+ for await (const middleware of this.stack) {
44
21
  try {
45
- middlewareFunc(context, nextFunc, doneFunc)
46
- } catch (err) {
47
- // Maintaining the existing error interface (Response object)
48
- self.robot.emit('error', err, context.response)
49
- // Forcibly fail the middleware and stop executing deeper
50
- doneFunc()
22
+ shouldContinue = await middleware(context)
23
+ if (shouldContinue === false) break
24
+ } catch (e) {
25
+ this.robot.emit('error', e, context.response)
26
+ break
51
27
  }
52
28
  }
53
-
54
- // Executed when the middleware stack is finished
55
- function allDone (_, finalDoneFunc) {
56
- next(context, finalDoneFunc)
57
- }
58
-
59
- // Execute each piece of middleware, collecting the latest 'done' callback
60
- // at each step.
61
- process.nextTick(async.reduce.bind(null, this.stack, done, executeSingleMiddleware, allDone))
29
+ return shouldContinue
62
30
  }
63
31
 
64
32
  // Public: Registers new middleware
65
33
  //
66
- // middleware - A generic pipeline component function that can either
67
- // continue the pipeline or interrupt it. The function is called
68
- // with (robot, context, next, done). If execution should
69
- // continue (next middleware, final callback), the middleware
70
- // should call the 'next' function with 'done' as an optional
71
- // argument.
72
- // If not, the middleware should call the 'done' function with
73
- // no arguments. Middleware may wrap the 'done' function in
74
- // order to execute logic after the final callback has been
75
- // executed.
34
+ // middleware - Middleware function to execute prior to the listener callback. Return false to prevent execution of the listener callback.
76
35
  //
77
36
  // Returns nothing.
78
37
  register (middleware) {
79
- if (middleware.length !== 3) {
80
- throw new Error(`Incorrect number of arguments for middleware callback (expected 3, got ${middleware.length})`)
38
+ if (middleware.length !== 1) {
39
+ throw new Error(`Incorrect number of arguments for middleware callback (expected 1, got ${middleware.length})`)
81
40
  }
82
41
  this.stack.push(middleware)
83
42
  }
package/src/response.js CHANGED
@@ -14,8 +14,7 @@ class Response {
14
14
  this.match = match
15
15
  this.envelope = {
16
16
  room: this.message.room,
17
- user: this.message.user,
18
- message: this.message
17
+ user: this.message.user
19
18
  }
20
19
  }
21
20
 
@@ -24,10 +23,9 @@ class Response {
24
23
  // strings - One or more strings to be posted. The order of these strings
25
24
  // should be kept intact.
26
25
  //
27
- // Returns nothing.
28
- send (/* ...strings */) {
29
- const strings = [].slice.call(arguments)
30
- this.runWithMiddleware.apply(this, ['send', { plaintext: true }].concat(strings))
26
+ // Returns result from middleware.
27
+ async send (...strings) {
28
+ return await this.#runWithMiddleware('send', { plaintext: true }, ...strings)
31
29
  }
32
30
 
33
31
  // Public: Posts an emote back to the chat source
@@ -35,10 +33,9 @@ class Response {
35
33
  // strings - One or more strings to be posted. The order of these strings
36
34
  // should be kept intact.
37
35
  //
38
- // Returns nothing.
39
- emote (/* ...strings */) {
40
- const strings = [].slice.call(arguments)
41
- this.runWithMiddleware.apply(this, ['emote', { plaintext: true }].concat(strings))
36
+ // Returns result from middleware.
37
+ async emote (...strings) {
38
+ return await this.#runWithMiddleware('emote', { plaintext: true }, ...strings)
42
39
  }
43
40
 
44
41
  // Public: Posts a message mentioning the current user.
@@ -46,10 +43,9 @@ class Response {
46
43
  // strings - One or more strings to be posted. The order of these strings
47
44
  // should be kept intact.
48
45
  //
49
- // Returns nothing.
50
- reply (/* ...strings */) {
51
- const strings = [].slice.call(arguments)
52
- this.runWithMiddleware.apply(this, ['reply', { plaintext: true }].concat(strings))
46
+ // Returns result from middleware.
47
+ async reply (...strings) {
48
+ return await this.#runWithMiddleware('reply', { plaintext: true }, ...strings)
53
49
  }
54
50
 
55
51
  // Public: Posts a topic changing message
@@ -57,10 +53,9 @@ class Response {
57
53
  // strings - One or more strings to set as the topic of the
58
54
  // room the bot is in.
59
55
  //
60
- // Returns nothing.
61
- topic (/* ...strings */) {
62
- const strings = [].slice.call(arguments)
63
- this.runWithMiddleware.apply(this, ['topic', { plaintext: true }].concat(strings))
56
+ // Returns result from middleware.
57
+ async topic (...strings) {
58
+ return await this.#runWithMiddleware('topic', { plaintext: true }, ...strings)
64
59
  }
65
60
 
66
61
  // Public: Play a sound in the chat source
@@ -68,10 +63,9 @@ class Response {
68
63
  // strings - One or more strings to be posted as sounds to play. The order of
69
64
  // these strings should be kept intact.
70
65
  //
71
- // Returns nothing
72
- play (/* ...strings */) {
73
- const strings = [].slice.call(arguments)
74
- this.runWithMiddleware.apply(this, ['play'].concat(strings))
66
+ // Returns result from middleware.
67
+ async play (...strings) {
68
+ return await this.#runWithMiddleware('play', {}, ...strings)
75
69
  }
76
70
 
77
71
  // Public: Posts a message in an unlogged room
@@ -79,27 +73,17 @@ class Response {
79
73
  // strings - One or more strings to be posted. The order of these strings
80
74
  // should be kept intact.
81
75
  //
82
- // Returns nothing
83
- locked (/* ...strings */) {
84
- const strings = [].slice.call(arguments)
85
- this.runWithMiddleware.apply(this, ['locked', { plaintext: true }].concat(strings))
76
+ // Returns result from middleware.
77
+ async locked (...strings) {
78
+ await this.#runWithMiddleware('locked', { plaintext: true }, ...strings)
86
79
  }
87
80
 
88
- // Private: Call with a method for the given strings using response
81
+ // Call with a method for the given strings using response
89
82
  // middleware.
90
- runWithMiddleware (methodName, opts/* , ...strings */) {
91
- const self = this
92
- const strings = [].slice.call(arguments, 2)
93
- const copy = strings.slice(0)
94
- let callback
95
-
96
- if (typeof copy[copy.length - 1] === 'function') {
97
- callback = copy.pop()
98
- }
99
-
83
+ async #runWithMiddleware (methodName, opts, ...strings) {
100
84
  const context = {
101
85
  response: this,
102
- strings: copy,
86
+ strings,
103
87
  method: methodName
104
88
  }
105
89
 
@@ -107,17 +91,9 @@ class Response {
107
91
  context.plaintext = true
108
92
  }
109
93
 
110
- function responseMiddlewareDone () {}
111
- function runAdapterSend (_, done) {
112
- const result = context.strings
113
- if (callback != null) {
114
- result.push(callback)
115
- }
116
- self.robot.adapter[methodName].apply(self.robot.adapter, [self.envelope].concat(result))
117
- done()
118
- }
119
-
120
- return this.robot.middleware.response.execute(context, runAdapterSend, responseMiddlewareDone)
94
+ const shouldContinue = await this.robot.middleware.response.execute(context)
95
+ if (shouldContinue === false) return
96
+ return await this.robot.adapter[methodName](this.envelope, ...context.strings)
121
97
  }
122
98
 
123
99
  // Public: Picks a random item from the given items.
package/src/robot.js CHANGED
@@ -4,7 +4,6 @@ const fs = require('fs')
4
4
  const path = require('path')
5
5
  const pathToFileURL = require('url').pathToFileURL
6
6
 
7
- const async = require('async')
8
7
  const pino = require('pino')
9
8
  const HttpClient = require('./httpclient')
10
9
 
@@ -51,11 +50,6 @@ class Robot {
51
50
  name,
52
51
  level: process.env.HUBOT_LOG_LEVEL || 'info'
53
52
  })
54
- Reflect.defineProperty(this.logger, 'warning', {
55
- value: this.logger.warn,
56
- enumerable: true,
57
- configurable: true
58
- })
59
53
 
60
54
  this.pingIntervalId = null
61
55
  this.globalHttpOptions = {}
@@ -73,10 +67,6 @@ class Robot {
73
67
  this.on('error', (err, res) => {
74
68
  return this.invokeErrorHandlers(err, res)
75
69
  })
76
- this.onUncaughtException = err => {
77
- return this.emit('error', err)
78
- }
79
- process.on('uncaughtException', this.onUncaughtException)
80
70
  }
81
71
 
82
72
  // Public: Adds a custom Listener with the provided matcher, options, and
@@ -233,9 +223,8 @@ class Robot {
233
223
  options = {}
234
224
  }
235
225
 
236
- this.listen(isCatchAllMessage, options, function listenCallback (msg) {
237
- msg.message = msg.message.message
238
- callback(msg)
226
+ this.listen(isCatchAllMessage, options, async msg => {
227
+ await callback(msg.message)
239
228
  })
240
229
  }
241
230
 
@@ -244,11 +233,8 @@ class Robot {
244
233
  //
245
234
  // middleware - A function that determines whether or not a given matching
246
235
  // Listener should be executed. The function is called with
247
- // (context, next, done). If execution should
248
- // continue (next middleware, Listener callback), the middleware
249
- // should call the 'next' function with 'done' as an argument.
250
- // If not, the middleware should call the 'done' function with
251
- // no arguments.
236
+ // (context). If execution should, the middleware should return
237
+ // true. If not, the middleware should return false.
252
238
  //
253
239
  // Returns nothing.
254
240
  listenerMiddleware (middleware) {
@@ -260,9 +246,8 @@ class Robot {
260
246
  //
261
247
  // middleware - A function that examines an outgoing message and can modify
262
248
  // it or prevent its sending. The function is called with
263
- // (context, next, done). If execution should continue,
264
- // the middleware should call next(done). If execution should
265
- // stop, the middleware should call done(). To modify the
249
+ // (context). If execution should continue, return true
250
+ // otherwise return false to stop. To modify the
266
251
  // outgoing message, set context.string to a new message.
267
252
  //
268
253
  // Returns nothing.
@@ -273,11 +258,10 @@ class Robot {
273
258
  // Public: Registers new middleware for execution before matching
274
259
  //
275
260
  // middleware - A function that determines whether or not listeners should be
276
- // checked. The function is called with (context, next, done). If
277
- // ext, next, done). If execution should continue to the next
278
- // middleware or matching phase, it should call the 'next'
279
- // function with 'done' as an argument. If not, the middleware
280
- // should call the 'done' function with no arguments.
261
+ // checked. The function is called with (context). If execution
262
+ // should continue to the next
263
+ // middleware or matching phase, it should return true or nothing
264
+ // otherwise return false to stop.
281
265
  //
282
266
  // Returns nothing.
283
267
  receiveMiddleware (middleware) {
@@ -290,14 +274,12 @@ class Robot {
290
274
  // message - A Message instance. Listeners can flag this message as 'done' to
291
275
  // prevent further execution.
292
276
  //
293
- // cb - Optional callback that is called when message processing is complete
294
- //
295
- // Returns nothing.
296
- // Returns before executing callback
297
- receive (message, cb) {
298
- // When everything is finished (down the middleware stack and back up),
299
- // pass control back to the robot
300
- this.middleware.receive.execute({ response: new Response(this, message) }, this.processListeners.bind(this), cb)
277
+ // Returns array of results from listeners.
278
+ async receive (message) {
279
+ const context = { response: new Response(this, message) }
280
+ const shouldContinue = await this.middleware.receive.execute(context)
281
+ if (shouldContinue === false) return null
282
+ return await this.processListeners(context)
301
283
  }
302
284
 
303
285
  // Private: Passes the given message to any interested Listeners.
@@ -305,45 +287,40 @@ class Robot {
305
287
  // message - A Message instance. Listeners can flag this message as 'done' to
306
288
  // prevent further execution.
307
289
  //
308
- // done - Optional callback that is called when message processing is complete
309
- //
310
- // Returns nothing.
311
- // Returns before executing callback
312
- processListeners (context, done) {
290
+ // Returns array of results from listeners.
291
+ async processListeners (context) {
313
292
  // Try executing all registered Listeners in order of registration
314
293
  // and return after message is done being processed
294
+ const results = []
315
295
  let anyListenersExecuted = false
316
-
317
- async.detectSeries(this.listeners, (listener, done) => {
296
+ for await (const listener of this.listeners) {
318
297
  try {
319
- listener.call(context.response.message, this.middleware.listener, function (listenerExecuted) {
320
- anyListenersExecuted = anyListenersExecuted || listenerExecuted
321
- // Defer to the event loop at least after every listener so the
322
- // stack doesn't get too big
323
- process.nextTick(() =>
324
- // Stop processing when message.done == true
325
- done(null, context.response.message.done)
326
- )
327
- })
298
+ const match = listener.matcher(context.response.message)
299
+ if (!match) {
300
+ continue
301
+ }
302
+ const result = await listener.call(context.response.message, this.middleware.listener)
303
+ results.push(result)
304
+ anyListenersExecuted = true
328
305
  } catch (err) {
329
- this.emit('error', err, new this.Response(this, context.response.message, []))
330
- // Continue to next listener when there is an error
331
- done(null, false)
306
+ this.emit('error', err, context)
332
307
  }
333
- },
334
- // Ignore the result ( == the listener that set message.done = true)
335
- _ => {
336
- // If no registered Listener matched the message
337
-
338
- if (!(context.response.message instanceof Message.CatchAllMessage) && !anyListenersExecuted) {
339
- this.logger.debug('No listeners executed; falling back to catch-all')
340
- this.receive(new Message.CatchAllMessage(context.response.message), done)
341
- } else {
342
- if (done != null) {
343
- process.nextTick(done)
344
- }
308
+ if (context.response.message.done) {
309
+ break
345
310
  }
346
- })
311
+ }
312
+
313
+ if (!isCatchAllMessage(context.response.message) && !anyListenersExecuted) {
314
+ this.logger.debug('No listeners executed; falling back to catch-all')
315
+ try {
316
+ const result = await this.receive(new Message.CatchAllMessage(context.response.message))
317
+ results.push(result)
318
+ } catch (err) {
319
+ this.emit('error', err, context)
320
+ }
321
+ }
322
+
323
+ return results
347
324
  }
348
325
 
349
326
  async loadmjs (filePath) {
@@ -389,7 +366,7 @@ class Robot {
389
366
  this.parseHelp(full)
390
367
  } catch (error) {
391
368
  this.logger.error(`Unable to load ${full}: ${error.stack}`)
392
- process.exit(1)
369
+ throw error
393
370
  }
394
371
  }
395
372
 
@@ -424,7 +401,7 @@ class Robot {
424
401
  Object.keys(packages).forEach(key => require(key)(this, packages[key]))
425
402
  } catch (error) {
426
403
  this.logger.error(`Error loading scripts from npm package - ${error.stack}`)
427
- process.exit(1)
404
+ throw error
428
405
  }
429
406
  }
430
407
 
@@ -472,9 +449,8 @@ class Robot {
472
449
  this.server = app.listen(port, address)
473
450
  this.router = app
474
451
  } catch (error) {
475
- const err = error
476
- this.logger.error(`Error trying to start HTTP server: ${err}\n${err.stack}`)
477
- process.exit(1)
452
+ this.logger.error(`Error trying to start HTTP server: ${error}\n${error.stack}`)
453
+ throw error
478
454
  }
479
455
 
480
456
  let herokuUrl = process.env.HEROKU_URL
@@ -533,9 +509,9 @@ class Robot {
533
509
  }
534
510
  }
535
511
  }
536
- } catch (err) {
537
- this.logger.error(`Cannot load adapter ${adapterPath ?? '[no path set]'} ${this.adapterName} - ${err}`)
538
- process.exit(1)
512
+ } catch (error) {
513
+ this.logger.error(`Cannot load adapter ${adapterPath ?? '[no path set]'} ${this.adapterName} - ${error}`)
514
+ throw error
539
515
  }
540
516
  }
541
517
 
@@ -616,10 +592,8 @@ class Robot {
616
592
  // strings - One or more Strings for each message to send.
617
593
  //
618
594
  // Returns whatever the extending adapter returns.
619
- send (envelope/* , ...strings */) {
620
- const strings = [].slice.call(arguments, 1)
621
-
622
- return this.adapter.send.apply(this.adapter, [envelope].concat(strings))
595
+ async send (envelope, ...strings) {
596
+ return await this.adapter.send(envelope, ...strings)
623
597
  }
624
598
 
625
599
  // Public: A helper reply function which delegates to the adapter's reply
@@ -629,10 +603,8 @@ class Robot {
629
603
  // strings - One or more Strings for each message to send.
630
604
  //
631
605
  // Returns whatever the extending adapter returns.
632
- reply (envelope/* , ...strings */) {
633
- const strings = [].slice.call(arguments, 1)
634
-
635
- return this.adapter.reply.apply(this.adapter, [envelope].concat(strings))
606
+ async reply (envelope, ...strings) {
607
+ return await this.adapter.reply(envelope, ...strings)
636
608
  }
637
609
 
638
610
  // Public: A helper send function to message a room that the robot is in.
@@ -641,11 +613,9 @@ class Robot {
641
613
  // strings - One or more Strings for each message to send.
642
614
  //
643
615
  // Returns whatever the extending adapter returns.
644
- messageRoom (room/* , ...strings */) {
645
- const strings = [].slice.call(arguments, 1)
616
+ async messageRoom (room, ...strings) {
646
617
  const envelope = { room }
647
-
648
- return this.adapter.send.apply(this.adapter, [envelope].concat(strings))
618
+ return await this.adapter.send(envelope, ...strings)
649
619
  }
650
620
 
651
621
  // Public: A wrapper around the EventEmitter API to make usage
@@ -656,10 +626,8 @@ class Robot {
656
626
  // when event happens.
657
627
  //
658
628
  // Returns nothing.
659
- on (event/* , ...args */) {
660
- const args = [].slice.call(arguments, 1)
661
-
662
- this.events.on.apply(this.events, [event].concat(args))
629
+ on (event, ...args) {
630
+ this.events.on(event, ...args)
663
631
  }
664
632
 
665
633
  // Public: A wrapper around the EventEmitter API to make usage
@@ -669,19 +637,17 @@ class Robot {
669
637
  // args... - Arguments emitted by the event
670
638
  //
671
639
  // Returns nothing.
672
- emit (event/* , ...args */) {
673
- const args = [].slice.call(arguments, 1)
674
-
675
- this.events.emit.apply(this.events, [event].concat(args))
640
+ emit (event, ...args) {
641
+ this.events.emit(event, ...args)
676
642
  }
677
643
 
678
644
  // Public: Kick off the event loop for the adapter
679
645
  //
680
- // Returns nothing.
681
- run () {
646
+ // Returns whatever the adapter returns.
647
+ async run () {
682
648
  this.emit('running')
683
649
 
684
- this.adapter.run()
650
+ return await this.adapter.run()
685
651
  }
686
652
 
687
653
  // Public: Gracefully shutdown the robot process
@@ -691,13 +657,12 @@ class Robot {
691
657
  if (this.pingIntervalId != null) {
692
658
  clearInterval(this.pingIntervalId)
693
659
  }
694
- process.removeListener('uncaughtException', this.onUncaughtException)
695
- this.adapter.close()
660
+ this.adapter?.close()
696
661
  if (this.server) {
697
662
  this.server.close()
698
663
  }
699
-
700
664
  this.brain.close()
665
+ this.events.removeAllListeners()
701
666
  }
702
667
 
703
668
  // Public: The version of Hubot from npm