Skip to content
This repository has been archived by the owner on Sep 22, 2024. It is now read-only.

Hubot 脚本语法

1dot75cm edited this page Jun 26, 2016 · 3 revisions

译自:hubot/docs/scripting.md

目录

Hubot 是一个可扩展的机器人。您可以在 scripts 下编写自定义脚本,或为社区分享您的脚本。

脚本剖析

当创建 hubot,生成器将创建 scripts 目录,可以在该目录添加自定义脚本。创建脚本需要以下步骤:

  • hubot 默认从 src/scriptsscripts 载入脚本
  • 创建 coffeejs 文件
  • 导出函数:
module.exports = (robot) ->
  # your code here

Hear & respond

聊天机器人一般通过消息进行交互。robot 的 hearrespond 方法可以接受/发送消息至聊天室。它需要一个扩展正则表达式和 callback 函数作为参数。例如:

module.exports = (robot) ->
  robot.hear /badger/i, (res) ->
    # your code here

  robot.respond /open the pod bay doors/i, (res) ->
    # your code here

robot.hear /badger/ 回调函数可以被任何匹配的消息调用。例如:

  • Stop badgering the witness
  • badger me
  • what exactly is a badger anyways

robot.respond /open the pod bay doors/i 回调函数只能被发送给 bot 的消息调用。如果 bot 名为 HAL,别名为 /,以下消息触发回调函数:

  • hal open the pod bay doors
  • HAL: open the pod bay doors
  • @HAL open the pod bay doors
  • /open the pod bay doors

以下消息不能触发回调函数:

  • HAL: please open the pod bay doors # 消息文本不匹配
  • has anyone ever mentioned how lovely you are when you open the pod bay doors? # 缺少机器人名称

Send & reply

res 参数是 Response 实例 (之前用 msg)。如果要从机器人返回消息,可使用 send, reply, emote 方法。

  • send 将消息发送到整个房间或群组;
  • reply 将消息回复给某人;
  • emote 强调消息,需 adapter 支持。
module.exports = (robot) ->
  robot.hear /badger/i, (res) ->
    res.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS"

  robot.respond /open the pod bay doors/i, (res) ->
    res.reply "I'm afraid I can't let you do that."

  robot.hear /I like pie/i, (res) ->
    res.emote "makes a freshly baked pie"

Capturing data

使用 res.match 捕获正则表达式匹配的消息内容,并返回包含结果的数组。这是 JavaScript 内置功能,数组索引 0 为匹配的全部内容。让我们来更新之前的脚本:

  robot.respond /open the (.*) doors/i, (res) ->
    doorType = res.match[1]
    if doorType is "pod bay"
      res.reply "I'm afraid I can't let you do that."
    else
      res.reply "Opening #{doorType} doors"

如果有人说 "HAL: open the pod bay doors",则 res.match[0] 为 "open the pod bay doors", res.match[1] 为 "pod bay"。

Making HTTP calls

Hubot 可通过 HTTP 集成第三方 API。robot.httpnode-scoped-http-client 的实例。示例如下:

  robot.http("https://midnight-train")
    .get() (err, res, body) ->  # err错误消息, res响应头, body响应内容
      if err
        res.send "Encountered an error :( #{err}"
        return
      # your code here, knowing it was successful
      res.send "Got back #{body}"

POST 示例如下:

  data = JSON.stringify({
    foo: 'bar'
  })
  robot.http("https://midnight-train")
    .header('Content-Type', 'application/json')
    .post(data) (err, res, body) ->
      # your code here

reshttp.ServerResponse 实例。我们只需要关注 statusCode, getHeader 方法。使用 statusCode 检查 HTTP 状态码,非 200 意味着发生错误。使用 getHeader 获取响应头,可检查速率限制:

  robot.http("https://midnight-train")
    .get() (err, res, body) ->
      # pretend there's error checking code here

      if res.statusCode isnt 200
        res.send "Request didn't come back HTTP 200 :("
        return

      rateLimitRemaining = parseInt res.getHeader('X-RateLimit-Limit') if res.getHeader('X-RateLimit-Limit')
      if rateLimitRemaining and rateLimitRemaining < 1
        res.send "Rate Limit hit, stop believing for awhile"

      # rest of your code

JSON

调用 API 最简洁的方式是返回 JSON,它不需要额外依赖。当调用 robot.http ,你可以通过 Accept 头设置返回的数据格式。一旦 body 返回,可使用 JSON.parse 解析:

  robot.http("https://midnight-train")
    .header('Accept', 'application/json')
    .get() (err, res, body) ->
      # error checking code here

      data = JSON.parse body
      res.send "#{data.passenger} taking midnight train going #{data.destination}"

body 可能返回非 JSON 数据,例如 API 遇到错误返回 HTML。为安全起见,你需要检查 Content-Type,并捕捉解析错误。

  robot.http("https://midnight-train")
    .header('Accept', 'application/json')
    .get() (err, res, body) ->
      # err & response status checking code here

      if response.getHeader('Content-Type') isnt 'application/json'
        res.send "Didn't get back JSON :("
        return

      data = null
      try
        data = JSON.parse body
      catch error
       res.send "Ran into an error parsing JSON :("
       return

      # your code here

XML

XML 没有封装好的解析库,所以解析比较困难。这超出了本文档的范围,以下库可供参考:

  • xml2json (simplest to use, but has some limitations)
  • jsdom (JavaScript implementation of the W3C DOM)
  • xml2js

Screen scraping

对于未提供 API 的页面,只能抓取页面。这超出了本文档的范围,以下库可供参考:

  • cheerio (familiar syntax and API to jQuery)
  • jsdom (JavaScript implementation of the W3C DOM)

Advanced HTTP and HTTPS settings

如上所述,hubot 使用 node-scoped-http-client 为创建 HTTP/HTTPS 请求提供简单接口。本节,使用 node 内置的 httphttps 库,它为几种常用的操作提供了简单的 DSL。

如果你需要直接控制 http/https 选项,可以向 robot.http 传入第二个参数,该参数将通过 node-scoped-http-client 传给 http/https。

  options =
    # don't verify server certificate against a CA, SCARY!
    rejectUnauthorized: false
  robot.http("https://midnight-train", options)

另外,如果 node-scoped-http-client 不能满足你,可以使用 http, https, request 或其他node库。

Random

常见模式是使用 hear 或 respond 命令,并从数组中取随机项进行回复。用 JavaScript 和 CoffeeScript 实现比较麻烦,所以 Hubot 提供了该方法:

lulz = ['lol', 'rofl', 'lmao']
res.send res.random lulz

Topic

Hubot 可响应房间主题的更改,需要 adapter 支持。

module.exports = (robot) ->
  robot.topic (res) ->
    res.send "#{res.message.text}? That's a Paddlin'"

Entering and leaving

Hubot 可响应用户进入和离开,需要 adapter 支持。

enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
leaveReplies = ['Are you still there?', 'Target lost', 'Searching']

module.exports = (robot) ->
  robot.enter (res) ->
    res.send res.random enterReplies
  robot.leave (res) ->
    res.send res.random leaveReplies

Custom Listeners

虽然上述功能可以满足大部分用户的需求(hear, respond, enter, leave, topic),但有时您可能需要非常特殊的逻辑匹配。此时,可以使用 listen 方法指定自定义匹配函数,代替之前的正则表达式。

如果执行 listener 回调函数,则匹配函数必须返回真值。匹配函数的返回值被传入回调函数,并作为 response.match 的值。

module.exports = (robot) ->
  robot.listen(
    (message) -> # Match function
      # Occassionally respond to things that Steve says
      message.user.name is "Steve" and Math.random() > 0.8
    (response) -> # Standard listener callback
      # Let Steve know how happy you are that he exists
      response.reply "HI STEVE! YOU'RE MY BEST FRIEND! (but only like #{response.match * 100}% of the time)"
  )

复杂的匹配示例可参考模式设计文档

Environment variables

Hubot可使用 process.env 访问环境变量。通常用于提供脚本参数,惯例是使用 HUBOT_ 前缀。

answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING or 42

module.exports = (robot) ->
  robot.respond /what is the answer to the ultimate question of life/, (res) ->
    res.send "#{answer}, but what is the question?"

注意,请确保设置变量默认值,保证未定义变量时脚本可以正常载入。建议脚本最好不需要其他配置便可以工作。

这将会退出,如果未定义变量:

answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
unless answer?
  console.log "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again"
  process.exit(1)

module.exports = (robot) ->
  robot.respond /what is the answer to the ultimate question of life/, (res) ->
    res.send "#{answer}, but what is the question?"

更新 robot.respond 来检查变量:

answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING

module.exports = (robot) ->
  robot.respond /what is the answer to the ultimate question of life/, (res) ->
    unless answer?
      res.send "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again"
      return
    res.send "#{answer}, but what is the question?"

Dependencies

Hubot 使用 npm 管理依赖。额外依赖需要添加至 package.json。例如,添加lolimadeupthispackage:

  "dependencies": {
    "hubot":         "2.5.5",
    "lolimadeupthispackage": "1.2.3"
  },

如果您使用 hubot-scripts 脚本,注意需要将脚本文档注明的依赖写入 package.json。请确保以有效的 JSON 格式添加依赖。

Timeouts and Intervals

Hubot 可使用 JavaScript 内置 setTimeout 设置超时。它是一个回调方法,调用回调函数前需要等待。

module.exports = (robot) ->
  robot.respond /you are a little slow/, (res) ->
    setTimeout () ->
      res.send "Who you calling 'slow'?"
    , 60 * 1000

Hubot 可使用 setInterval 设置间隔。它是一个回调方法,调用回调函数前需要等待。

module.exports = (robot) ->
  robot.respond /annoy me/, (res) ->
    res.send "Hey, want to hear the most annoying sound in the world?"
    setInterval () ->
      res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
    , 1000

setTimeoutsetInterval 返回已创建超时或间隔的 ID。该 ID 用于 clearTimeoutclearInterval

module.exports = (robot) ->
  annoyIntervalId = null

  robot.respond /annoy me/, (res) ->
    if annoyIntervalId
      res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
      return

    res.send "Hey, want to hear the most annoying sound in the world?"
    annoyIntervalId = setInterval () ->
      res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
    , 1000

  robot.respond /unannoy me/, (res) ->
    if annoyIntervalId
      res.send "GUYS, GUYS, GUYS!"
      clearInterval(annoyIntervalId) ->
      annoyIntervalId = null
    else
      res.send "Not annoying you right now, am I?"

HTTP Listener

Hubot 支持 express web 框架来响应 HTTP 请求。它监听 EXPRESS_PORTPORT 环境变量指定的端口,默认 8080robot.router 是 express 应用实例。它可以通过指定 EXPRESS_USEREXPRESS_PASSWORD 保护用户名和密码。设置 EXPRESS_STATIC 来提供静态文件。

最常见的用途是为 Webhook 推送服务提供 HTTP 端,并在 chat 中显示。

module.exports = (robot) ->
  # the expected value of :room is going to vary by adapter, it might be a numeric id, name, token, or some other value
  robot.router.post '/hubot/chatsecrets/:room', (req, res) ->
    room   = req.params.room
    data   = if req.body.payload? then JSON.parse req.body.payload else req.body
    secret = data.secret

    robot.messageRoom room, "I have a secret: #{secret}"

    res.send 'OK'

使用 curl 测试;请查看 error handling 部分。

// raw json, must specify Content-Type: application/json
curl -X POST -H "Content-Type: application/json" -d '{"secret":"C-TECH Astronomy"}' http://127.0.0.1:8080/hubot/chatsecrets/general

// defaults Content-Type: application/x-www-form-urlencoded, must st payload=...
curl -d 'payload=%7B%22secret%22%3A%22C-TECH+Astronomy%22%7D' http://127.0.0.1:8080/hubot/chatsecrets/general

所有端点 URL 应以 /hubot 字符串开始(无论 robot 名称是什么)。这种一致性使它更容易创建 webhooks(可复制URL)并保证网址有效性(robot 名称可能不是安全的 URL 字符串)。

Events

Hubot 还可以响应事件,用于在脚本间传递数据。robot.emitrobot.on 方法通过封装 EventEmitter 模块获得。

示例:有一个脚本用于处理与服务的交互,然后发射事件。例如,我们有个脚本用于从 GitHub post-commit hook 接收数据,然后发射 commits 通知,最后由另一个脚本用这些 commits 执行操作。

# src/scripts/github-commits.coffee
module.exports = (robot) ->
  robot.router.post "/hubot/gh-commits", (req, res) ->
    robot.emit "commit", {
        user    : {}, #hubot user object
        repo    : 'https://github.com/github/hubot',
        hash  : '2e1951c089bd865839328592ff673d2f08153643'
    }
# src/scripts/heroku.coffee
module.exports = (robot) ->
  robot.on "commit", (commit) ->
    robot.send commit.user, "Will now deploy #{commit.hash} from #{commit.repo}!"
    #deploy code goes here

如果你提供一个事件,建议你在数据中包含 hubot userroom 对象。这将允许 hubot 提醒聊天室中的 userroom

Error Handling

没有代码是完美的,错误和异常是可预知的。未捕获的异常会导致 hubot 崩溃。Hubot 现在包含一个 uncaughtException handler,为脚本提供一些异常处理能力。

# src/scripts/does-not-compute.coffee
module.exports = (robot) ->
  robot.error (err, res) ->
    robot.logger.error "DOES NOT COMPUTE"

    if res?
      res.reply "DOES NOT COMPUTE"

在这里你可以做任何事,但你需要记录错误作为额外的措施,特别是异步代码。否则,可能你会遇到递归错误,但不知道如何解决。

在内部,有一个 'error' 事件发出,错误处理程序接收该事件。uncaughtException handler 处理未知状态的进程。因此,你应该定义自己的异常,并发射它们。第一个参数是抛出的错误,第二个是生成该错误可选的错误消息。

  robot.router.post '/hubot/chatsecrets/:room', (req, res) ->
    room = req.params.room
    data = null
    try
      data = JSON.parse req.body.payload
    catch err
      robot.emit 'error', err

    # rest of the code here


  robot.hear /midnight train/i, (res)
    robot.http("https://midnight-train")
      .get() (err, res, body) ->
        if err
          res.reply "Had problems taking the midnight train"
          robot.emit 'error', err, res
          return
        # rest of code here

第二个示例,你需要思考想要用户看到什么消息。如果直接返回 error handler 给用户,你不需要添加自定义消息,并将错误消息提供给 get() 请求。这依赖于你想用户如何报告异常。

Documenting Scripts

Hubot 脚本在文件开始的注释中编写文档,例如:

# Description:
#   <description of the scripts functionality>
#
# Dependencies:
#   "<module name>": "<module version>"
#
# Configuration:
#   LIST_OF_ENV_VARS_TO_SET
#
# Commands:
#   hubot <trigger> - <what the respond trigger does>
#   <trigger> - <what the hear trigger does>
#
# Notes:
#   <optional notes required for the script>
#
# Author:
#   <github username of the original script author>

其中最重要的是 Commands 部分。加载时,Hubot 查看每个脚本的 Commands 部分,并创建所有命令的列表。help.coffee 允许用户查看所有命令的帮助。因此,文档的 Commands 很重要。

关于文档commands,有些最佳实践:

  • 写在一行。帮助命令会被排序,因此不要换行,插入第二行没有任何意义。
  • 将 Hubot 写为 hubot,即使你的 hubot 被命名为其他名称。它会自动用正确的替换当前名称。这使得共享脚本而无需修改文档。
  • robot.respond 文档,前缀永远是 hubot。Hubot 将自动替换它为 robot 名称或别名。
  • 查看 man 手册。特别是,括号表示的可选部分,'...' 为任意数量的参数等。

其他部分与bot开发有关,特别是依赖关系,配置变量和注释。hubot-scripts 的贡献者应该包含与运行脚本相关的所有部分。

Persistence

Hubot 使用内存存储键值对,并导出为 robot.brain 用于通过脚本持久化存储相关数据。

robot.respond /have a soda/i, (res) ->
  # Get number of sodas had (coerced to a number).
  sodasHad = robot.brain.get('totalSodas') * 1 or 0

  if sodasHad > 4
    res.reply "I'm too fizzy.."

  else
    res.reply 'Sure!'

    robot.brain.set 'totalSodas', sodasHad+1
robot.respond /sleep it off/i, (res) ->
  robot.brain.set 'totalSodas', 0
  msg.reply 'zzzzz'

如果脚本需要查看用户数据,以下 robot.brain 方法可以查看多个用户的 id, name 或用户名匹配的 'fuzzy':userForName, userForId, userForFuzzyName, usersForFuzzyName

module.exports = (robot) ->

  robot.respond /who is @?([\w .\-]+)\?*$/i, (res) ->
    name = res.match[1].trim()

    users = robot.brain.usersForFuzzyName(name)
    if users.length is 1
      user = users[0]
      # Do something interesting here..

      res.send "#{name} is user - #{user}"

Script Loading

Hubot从三个地方载入脚本:

  • Hubot安装目录下的 scripts/ 目录;
  • hubot-scripts 包的社区脚本在 hubot-scripts.json 文件中指定;
  • 脚本从外部 npm 包载入,并在 external-scripts.json 文件指定。

scripts/ 目录的脚本按字母顺序加载。例如:

  • scripts/1-first.coffee
  • scripts/_second.coffee
  • scripts/third.coffee

Sharing Scripts

Once you've built some new scripts to extend the abilities of your robot friend, you should consider sharing them with the world! At the minimum, you need to package up your script and submit it to the Node.js Package Registry. You should also review the best practices for sharing scripts below.

See if a script already exists

Start by checking if an NPM package for a script like yours already exists. If you don't see an existing package that you can contribute to, then you can easily get started using the hubot script yeoman generator.

Creating A Script Package

Creating a script package for hubot is very simple. Start by installing the hubot yeoman generator:

% npm install -g yo generator-hubot

Once you've got the hubot generator installed, creating a hubot script is similar to creating a new hubot. You create a directory for your hubot script and generate a new hubot:script in it. For example, if we wanted to create a hubot script called "my-awesome-script":

% mkdir hubot-my-awesome-script
% cd hubot-my-awesome-script
% yo hubot:script

At this point, the you'll be asked a few questions about the author for the script, name of the script (which is guessed by the directory name), a short description, and keywords to find it (we suggest having at least hubot, hubot-scripts in this list).

If you are using git, the generated directory includes a .gitignore, so you can initialize and add everything:

% git init
% git add .
% git commit -m "Initial commit"

You now have a hubot script repository that's ready to roll! Feel free to crack open the pre-created src/awesome-script.coffee file and start building up your script! When you've got it ready, you can publish it to npmjs by following their documentation!

You'll probably want to write some unit tests for your new script. A sample test script is written to test/awesome-script-test.coffee, which you can run with grunt. For more information on tests, see the Testing Hubot Scripts section.

Listener Metadata

In addition to a regular expression and callback, the hear and respond functions also accept an optional options Object which can be used to attach arbitrary metadata to the generated Listener object. This metadata allows for easy extension of your script's behavior without modifying the script package.

The most important and most common metadata key is id. Every Listener should be given a unique name (options.id; defaults to null). Names should be scoped by module (e.g. 'my-module.my-listener'). These names allow other scripts to directly address individual listeners and extend them with additional functionality like authorization and rate limiting.

Additional extensions may define and handle additional metadata keys. For more information, see the Listener Middleware section.

Returning to an earlier example:

module.exports = (robot) ->
  robot.respond /annoy me/, id:'annoyance.start', (msg)
    # code to annoy someone

  robot.respond /unannoy me/, id:'annoyance.stop', (msg)
    # code to stop annoying someone

These scoped identifiers allow you to externally specify new behaviors like:

  • authorization policy: "allow everyone in the annoyers group to execute annoyance.* commands"
  • rate limiting: "only allow executing annoyance.start once every 30 minutes"

Middleware

There are three kinds of middleware: Receive, Listener and Response.

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

Execution Process and API

Similar to Express 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).

Middleware is called with:

  • context
    • See the each middleware type's API to see what the context will expose.
  • next
    • a Function with no additional properties that should be called to continue on to the next piece of middleware/execute the Listener callback
    • next should be called with a single, optional argument: either the provided done function or a new function that eventually calls done. If the argument is not given, the provided done will be assumed.
  • done
  • a Function with no additional properties that should be called to interrupt middleware execution and begin executing the chain of completion functions.
  • done should be called with no arguments

Every middleware receives the same API signature of context, next, and done. Different kinds of middleware may receive different information in the context object. For more details, see the API for each type of middleware.

Error Handling

For synchronous middleware (never yields to the event loop), hubot will automatically catch errors and emit an 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.

Listener Middleware

Listener middleware inserts logic between the listener matching a message and the listener executing. This allows you to create extensions that run for every matching script. Examples include centralized authorization policies, rate limiting, logging, and metrics. Middleware is implemented like other hubot scripts: instead of using the hear and respond methods, middleware is registered using listenerMiddleware.

Listener Middleware Examples

A fully functioning example can be found in hubot-rate-limit.

A simple example of middleware logging command executions:

module.exports = (robot) ->
  robot.listenerMiddleware (context, next, done) ->
    # Log commands
    robot.logger.info "#{context.response.message.user.name} asked me to #{context.response.message.text}"
    # Continue executing middleware
    next()

In this example, a log message will be written for each chat message that matches a Listener.

A more complex example making a rate limiting decision:

module.exports = (robot) ->
  # Map of listener ID to last time it was executed
  lastExecutedTime = {}

  robot.listenerMiddleware (context, next, done) ->
    try
      # Default to 1s unless listener provides a different minimum period
      minPeriodMs = context.listener.options?.rateLimits?.minPeriodMs? or 1000

      # See if command has been executed recently
      if lastExecutedTime.hasOwnProperty(context.listener.options.id) and
         lastExecutedTime[context.listener.options.id] > Date.now() - minPeriodMs
        # Command is being executed too quickly!
        done()
      else
        next ->
          lastExecutedTime[context.listener.options.id] = Date.now()
          done()
    catch err
      robot.emit('error', err, context.response)

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.

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.

module.exports = (robot) ->
  robot.hear /hello/, id: 'my-hello', rateLimits: {minPeriodMs: 10000}, (msg) ->
    # This will execute no faster than once every ten seconds
    msg.reply 'Why, hello there!'

Listener Middleware API

Listener middleware callbacks receive three arguments, context, next, and done. See the middleware API for a description of next and done. Listener middleware context includes these fields:

  • listener
    • options: a simple Object containing options set when defining the listener. See Listener Metadata.
    • all other properties should be considered internal
  • response
    • all parts of the standard response API are included in the middleware API. See Send & Reply.
    • middleware may decorate (but not modify) the response object with additional information (e.g. add a property to response.message.user with a user's LDAP groups)
    • note: the textual message (response.message.text) should be considered immutable in listener middleware

Receive Middleware

Receive middleware runs before any listeners have executed. It's suitable for blacklisting commands that have not been updated to add an ID, metrics, and more.

Receive Middleware Example

This simple middlware bans hubot use by a particular user, including hear listeners. If the user attempts to run a command explicitly, it will return an error message.

BLACKLISTED_USERS = [
  '12345' # Restrict access for a user ID for a contractor
]

robot.receiveMiddleware (context, next, done) ->
  if context.response.message.user.id in BLACKLISTED_USERS
    # Don't process this message further.
    context.response.message.finish()

    # If the message starts with 'hubot' or the alias pattern, this user was
    # explicitly trying to run a command, so respond with an error message.
    if context.response.message.text?.match(robot.respondPattern(''))
      context.response.reply "I'm sorry @#{context.response.message.user.name}, but I'm configured to ignore your commands."

    # Don't process further middleware.
    done()
  else
    next(done)

Receive Middleware API

Receive middleware callbacks receive three arguments, context, next, and done. See the middleware API for a description of next and done. Receive middleware context includes these fields:

  • response
    • this response object will not have a match property, as no listeners have been run yet to match it.
    • middleware may decorate the response object with additional information (e.g. add a property to response.message.user with a user's LDAP groups)
    • middleware may modify the response.message object

Response Middleware

Response middleware runs against every message hubot sends to a chat room. It's helpful for message formatting, preventing password leaks, metrics, and more.

Response Middleware Example

This simple example changes the format of links sent to a chat room from markdown links (like example) to the format supported by Slack, https://example.com|example.

module.exports = (robot) ->
  robot.responseMiddleware (context, next, done) ->
    return unless context.plaintext?
    context.strings = (string.replace(/\[([^\[\]]*?)\]\((https?:\/\/.*?)\)/, "<$2|$1>") for string in context.strings)
    next()

Response Middleware API

Response middleware callbacks receive three arguments, context, next, and done. See the middleware API for a description of next and done. Receive middleware context includes these fields:

  • response
    • 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.
  • strings
    • An array of strings being sent to the chat room adapter. You can edit these, or use context.strings = ["new strings"] to replace them.
  • method
    • A string representing which type of response message the listener sent, such as send, reply, emote or topic.
  • plaintext
    • 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.

Testing Hubot Scripts

hubot-test-helper 是一个用于 Hubot 脚本单元测试的框架(建议使用最新版本Node)。

为 Hubot 实例安装 hubot-test-helper:

% npm install hubot-test-helper --save-dev

还需要安装以下包:

  • 一个JavaScript测试框架,例如 Mocha
  • 一个断言库,例如 chaiexpect.js

可能需要安装:

  • coffee-script (如果需要测试CoffeeScript)
  • mock库,如 Sinon.js (如果你的脚本执行 webservice 调用或其他异步操作)

这里有一个简单的脚本,以及测试命令在 Hubot sample script。该脚本使用 Mocha, chai, coffee-scripthubot-test-helper

test/example-test.coffee

Helper = require('hubot-test-helper')
chai = require 'chai'

expect = chai.expect

helper = new Helper('../scripts/example.coffee')

describe 'example script', ->
  beforeEach ->
    @room = helper.createRoom()

  afterEach ->
    @room.destroy()

  it 'doesn\'t need badgers', ->
    @room.user.say('alice', 'did someone call for a badger?').then =>
      expect(@room.messages).to.eql [
        ['alice', 'did someone call for a badger?']
        ['hubot', 'Badgers? BADGERS? WE DON\'T NEED NO STINKIN BADGERS']
      ]

  it 'won\'t open the pod bay doors', ->
    @room.user.say('bob', '@hubot open the pod bay doors').then =>
      expect(@room.messages).to.eql [
        ['bob', '@hubot open the pod bay doors']
        ['hubot', '@bob I\'m afraid I can\'t let you do that.']
      ]

  it 'will open the dutch doors', ->
    @room.user.say('bob', '@hubot open the dutch doors').then =>
      expect(@room.messages).to.eql [
        ['bob', '@hubot open the dutch doors']
        ['hubot', '@bob Opening dutch doors']
      ]

sample output

% mocha --compilers "coffee:coffee-script/register" test/*.coffee


  example script
    ✓ doesn't need badgers
    ✓ won't open the pod bay doors
    ✓ will open the dutch doors


  3 passing (212ms)
Clone this wiki locally