Skip to content

Commit f8506cd

Browse files
author
Ben Lavender
committed
Merge pull request #1021 from github/response-middleware
Response middleware
2 parents e8a420e + 0544029 commit f8506cd

File tree

4 files changed

+159
-13
lines changed

4 files changed

+159
-13
lines changed

docs/scripting.md

+36-7
Original file line numberDiff line numberDiff line change
@@ -645,22 +645,18 @@ These scoped identifiers allow you to externally specify new behaviors like:
645645

646646
# Middleware
647647

648-
There are two kinds of middleware: Receive middleware and Listener Middleware.
648+
There are three kinds of middleware: Receive, Listener and Response.
649649

650650
Receive middleware runs once, before listeners are checked.
651651
Listener middleware runs for every listener that matches the message.
652+
Response middleware runs for every response sent to a message.
652653

653654
## Execution Process and API
654655

655-
Similar to [Express middleware](http://expressjs.com/api.html#middleware), Hubot listener middleware executes middleware in definition order. Each middleware can either continue the chain (by calling `next`) or interrupt the chain (by calling `done`). If all middleware continues, the listener callback is executed and `done` is called. Middleware may wrap the `done` callback to allow executing code in the second half of the process (after the listener callback has been executed or a deeper piece of middleware has interrupted).
656+
Similar to [Express middleware](http://expressjs.com/api.html#middleware), Hubot executes middleware in definition order. Each middleware can either continue the chain (by calling `next`) or interrupt the chain (by calling `done`). If all middleware continues, the listener callback is executed and `done` is called. Middleware may wrap the `done` callback to allow executing code in the second half of the process (after the listener callback has been executed or a deeper piece of middleware has interrupted).
656657

657658
Middleware is called with:
658659

659-
- a context object containing:
660-
- matching Listener object (with associated metadata)
661-
- response object (contains the original message)
662-
- next/done callbacks.
663-
664660
- `context`
665661
- See the each middleware type's API to see what the context will expose.
666662
- `next`
@@ -789,3 +785,36 @@ of `next` and `done`. Receive middleware context includes these fields:
789785
- this response object will not have a `match` property, as no listeners have been run yet to match it.
790786
- middleware may decorate the response object with additional information (e.g. add a property to `response.message.user` with a user's LDAP groups)
791787
- middleware may modify the `response.message` object
788+
789+
# Response Middleware
790+
791+
Response middleware runs against every message hubot sends to a chat room. It's
792+
helpful for message formatting, preventing password leaks, metrics, and more.
793+
794+
## Response Middleware Example
795+
796+
This simple example changes the format of links sent to a chat room from
797+
markdown links (like [example](https://example.com)) to the format supported
798+
by [Slack](https://slack.com), <https://example.com|example>.
799+
800+
```coffeescript
801+
module.exports = (robot) ->
802+
robot.responseMiddleware (context, next, done) ->
803+
return unless context.plaintext?
804+
context.strings = (string.replace(/\[([^\[\]]*?)\]\((https?:\/\/.*?)\)/, "<$2|$1>") for string in context.strings)
805+
next()
806+
```
807+
808+
## Response Middleware API
809+
810+
Response middleware callbacks receive three arguments, `context`, `next`, and
811+
`done`. See the [middleware API](#execution-process-and-api) for a description
812+
of `next` and `done`. Receive middleware context includes these fields:
813+
- `response`
814+
- This response object can be used to send new messages from the middleware. Middleware will be called on these new responses. Be careful not to create infinite loops.
815+
- `strings`
816+
- An array of strings being sent to the chat room adapter. You can edit these, or use `context.strings = ["new strings"]` to replace them.
817+
- `method`
818+
- A string representing which type of response message the listener sent, such as `send`, `reply`, `emote` or `topic`.
819+
- `plaintext`
820+
- `true` or `undefined`. This will be set to `true` if the message is of a normal plaintext type, such as `send` or `reply`. This property should be treated as read-only.

src/response.coffee

+25-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Response
1919
#
2020
# Returns nothing.
2121
send: (strings...) ->
22-
@robot.adapter.send @envelope, strings...
22+
@runWithMiddleware("send", { plaintext: true }, strings...)
2323

2424
# Public: Posts an emote back to the chat source
2525
#
@@ -28,7 +28,7 @@ class Response
2828
#
2929
# Returns nothing.
3030
emote: (strings...) ->
31-
@robot.adapter.emote @envelope, strings...
31+
@runWithMiddleware("emote", { plaintext: true }, strings...)
3232

3333
# Public: Posts a message mentioning the current user.
3434
#
@@ -37,7 +37,7 @@ class Response
3737
#
3838
# Returns nothing.
3939
reply: (strings...) ->
40-
@robot.adapter.reply @envelope, strings...
40+
@runWithMiddleware("reply", { plaintext: true }, strings...)
4141

4242
# Public: Posts a topic changing message
4343
#
@@ -46,7 +46,7 @@ class Response
4646
#
4747
# Returns nothing.
4848
topic: (strings...) ->
49-
@robot.adapter.topic @envelope, strings...
49+
@runWithMiddleware("topic", { plaintext: true }, strings...)
5050

5151
# Public: Play a sound in the chat source
5252
#
@@ -55,7 +55,7 @@ class Response
5555
#
5656
# Returns nothing
5757
play: (strings...) ->
58-
@robot.adapter.play @envelope, strings...
58+
@runWithMiddleware("play", strings...)
5959

6060
# Public: Posts a message in an unlogged room
6161
#
@@ -64,7 +64,26 @@ class Response
6464
#
6565
# Returns nothing
6666
locked: (strings...) ->
67-
@robot.adapter.locked @envelope, strings...
67+
@runWithMiddleware("locked", { plaintext: true }, strings...)
68+
69+
# Private: Call with a method for the given strings using response
70+
# middleware.
71+
runWithMiddleware: (methodName, opts, strings...) ->
72+
callback = undefined
73+
copy = strings.slice(0)
74+
if typeof(copy[copy.length - 1]) == 'function'
75+
callback = copy.pop()
76+
context = {response: @, strings: copy, method: methodName}
77+
context.plaintext = true if opts.plaintext?
78+
responseMiddlewareDone = ->
79+
runAdapterSend = (_, done) =>
80+
result = context.strings
81+
result.push(callback) if callback?
82+
@robot.adapter[methodName](@envelope, result...)
83+
done()
84+
@robot.middleware.response.execute context,
85+
runAdapterSend,
86+
responseMiddlewareDone
6887

6988
# Public: Picks a random item from the given items.
7089
#

src/robot.coffee

+16
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class Robot
5151
@listeners = []
5252
@middleware =
5353
listener: new Middleware(@)
54+
response: new Middleware(@)
5455
receive: new Middleware(@)
5556
@logger = new Log process.env.HUBOT_LOG_LEVEL or 'info'
5657
@pingIntervalId = null
@@ -249,6 +250,21 @@ class Robot
249250
@middleware.listener.register middleware
250251
return undefined
251252

253+
# Public: Registers new middleware for execution as a response to any
254+
# message is being sent.
255+
#
256+
# middleware - A function that examines an outgoing message and can modify
257+
# it or prevent its sending. The function is called with
258+
# (context, next, done). If execution should continue,
259+
# the middleware should call next(done). If execution should stop,
260+
# the middleware should call done(). To modify the outgoing message,
261+
# set context.string to a new message.
262+
#
263+
# Returns nothing.
264+
responseMiddleware: (middleware) ->
265+
@middleware.response.register middleware
266+
return undefined
267+
252268
# Public: Registers new middleware for execution before matching
253269
#
254270
# middleware - A function that determines whether or not listeners should be

test/robot_test.coffee

+82
Original file line numberDiff line numberDiff line change
@@ -767,3 +767,85 @@ describe 'Robot', ->
767767
@robot.receive testMessage, ->
768768
expect(testCallback).to.have.been.called
769769
testDone()
770+
771+
describe 'Response Middleware', ->
772+
it 'executes response middleware in order', (testDone) ->
773+
@robot.adapter.send = sendSpy = sinon.spy()
774+
listenerCallback = sinon.spy()
775+
@robot.hear /^message123$/, (response) ->
776+
response.send "foobar, sir, foobar."
777+
778+
@robot.responseMiddleware (context, next, done) ->
779+
context.strings[0] = context.strings[0].replace(/foobar/g, "barfoo")
780+
next()
781+
782+
@robot.responseMiddleware (context, next, done) ->
783+
context.strings[0] = context.strings[0].replace(/barfoo/g, "replaced bar-foo")
784+
next()
785+
786+
testMessage = new TextMessage @user, 'message123'
787+
@robot.receive testMessage, () ->
788+
expect(sendSpy.getCall(0).args[1]).to.equal('replaced bar-foo, sir, replaced bar-foo.')
789+
testDone()
790+
791+
it 'allows replacing outgoing strings', (testDone) ->
792+
@robot.adapter.send = sendSpy = sinon.spy()
793+
listenerCallback = sinon.spy()
794+
@robot.hear /^message123$/, (response) ->
795+
response.send "foobar, sir, foobar."
796+
797+
@robot.responseMiddleware (context, next, done) ->
798+
context.strings = ["whatever I want."]
799+
next()
800+
801+
testMessage = new TextMessage @user, 'message123'
802+
@robot.receive testMessage, () ->
803+
expect(sendSpy.getCall(0).args[1]).to.deep.equal("whatever I want.")
804+
testDone()
805+
806+
it 'marks plaintext as plaintext', (testDone) ->
807+
@robot.adapter.send = sendSpy = sinon.spy()
808+
listenerCallback = sinon.spy()
809+
@robot.hear /^message123$/, (response) ->
810+
response.send "foobar, sir, foobar."
811+
@robot.hear /^message456$/, (response) ->
812+
response.play "good luck with that"
813+
814+
method = undefined
815+
plaintext = undefined
816+
@robot.responseMiddleware (context, next, done) ->
817+
method = context.method
818+
plaintext = context.plaintext
819+
next(done)
820+
821+
testMessage = new TextMessage @user, 'message123'
822+
823+
@robot.receive testMessage, () =>
824+
expect(plaintext).to.equal true
825+
expect(method).to.equal "send"
826+
testMessage2 = new TextMessage @user, 'message456'
827+
@robot.receive testMessage2, () ->
828+
expect(plaintext).to.equal undefined
829+
expect(method).to.equal "play"
830+
testDone()
831+
832+
it 'does not send trailing functions to middleware', (testDone) ->
833+
@robot.adapter.send = sendSpy = sinon.spy()
834+
asserted = false
835+
postSendCallback = ->
836+
@robot.hear /^message123$/, (response) ->
837+
response.send "foobar, sir, foobar.", postSendCallback
838+
839+
@robot.responseMiddleware (context, next, done) ->
840+
# We don't send the callback function to middleware, so it's not here.
841+
expect(context.strings).to.deep.equal ["foobar, sir, foobar."]
842+
expect(context.method).to.equal "send"
843+
asserted = true
844+
next()
845+
846+
testMessage = new TextMessage @user, 'message123'
847+
@robot.receive testMessage, ->
848+
expect(asserted).to.equal(true)
849+
expect(sendSpy.getCall(0).args[1]).to.equal('foobar, sir, foobar.')
850+
expect(sendSpy.getCall(0).args[2]).to.equal(postSendCallback)
851+
testDone()

0 commit comments

Comments
 (0)