使えない Hubot アダプターをつくろう

これは [Hubot Advent Calendar 2014][hubot-adventar-2014] の 24 日目の記事です。

また、今回は @bouzuya の Hubot 連載の第 16 回です。[目次は、第 1 回の記事にある][hubot-adventar-2014-bouzuya-index]ので、そちらをどうぞ。

# 前回まで、そして今回は

前回は [Hubot 標準同梱の Shell アダプターを読んでみよう][hubot-adventar-2014-23] ということで、Hubot アダプターについて軽く紹介し、コードを読みました。

今回は Hubot アダプターを実際につくってみましょう。

# Chadventar という仮想チャット

今回のために [bouzuya/hubot-chadventar](https://github.com/bouzuya/hubot-chadventar) というアダプターをつくりました。動くかは分かりません。今回のためにつくった仮想チャットのための Hubot アダプターなので。

今回、想定しているのは Chadventar というチャットです。

`POST /messages` に `{ name: ..., text: ... }` で `JSON` を受け付けて、受信したメッセージをチャットに流します。

チャットに流れているメッセージは指定した URL (今回なら Hubot の `/chadventar/messages` ) に `POST` メソッドで `{ name: ..., text: ... }` の `JSON` で送られます。

では、見てみましょう。

# サンプルコード

```coffeescript:index.coffee
{Adapter, TextMessage} = require 'hubot'

config =
  baseUrl: process.env.HUBOT_CHADVENTAR_BASE_URL ? 'http://localhost:8888'

class Chadventar extends Adapter
  send: (envelope, strings...) ->
    data = JSON.stringify { name: envelope.name, text: strings.join('\n') }
    @robot.http(config.baseUrl + '/messages')
      .header('Content-Type', 'application/json')
      .post(data) (err) =>
        return @robot.logger.error(err) if err?

  run: ->
    @robot.router.post '/chadventar/messages', (req, res) =>
      {name, text} = req.body
      user = @robot.brain.userForId name, room: 'room'
      @receive new TextMessage(user, text, 'messageId')
      res.send 201

    @emit 'connected'

module.exports.use = (robot) ->
  new Chadventar(robot)
```

ソースコードは大したことないですね。

ほとんどは昨日紹介したとおりです。念のため解説を入れます。

## `module.exports.use`

```coffeescript
module.exports.use = (robot) ->
  new Chadventar(robot)
```

アダプターのインスタンスを返します。

## `Chadventar#run`

```coffeescript
  run: ->
    @robot.router.post '/chadventar/messages', (req, res) =>
      {name, text} = req.body
      user = @robot.brain.userForId name, room: 'room'
      @receive new TextMessage(user, text, 'messageId')
      res.send 201

    @emit 'connected'
```

アダプターの初期化処理をします。

今回は Web Hook を待ち受けるために `robot.router` を使って待ち受けています。メッセージを受信したら `robot.brain.userForId` でユーザーを get or new して brain に登録します。`Adapter#receive` で Hubot 内にメッセージを流します。

上記の待ち受け設定を完了したら `'connected'` を `emit` します。

## `Chadventar#send`

```coffeescript
  send: (envelope, strings...) ->
    data = JSON.stringify { name: envelope.name, text: strings.join('\n') }
    @robot.http(config.baseUrl + '/messages')
      .header('Content-Type', 'application/json')
      .post(data) (err) =>
        return @robot.logger.error(err) if err?
```

メッセージをチャットに送信する処理です。 `Robot#messageRoom` などの `room` には対応していません。

最後まで紹介できなかった、しなかった、標準のクソ HTTP クライアント [technoweenie/node-scoped-http-client](https://github.com/technoweenie/node-scoped-http-client) を使って、Chadventar サーバーにメッセージを送信しています。

# テストコード

動かすのが面倒なので、テストコードを書いています。解説する余裕がないので、コードを貼り付けておきます。おそらく前々回、軽く紹介しているので、分かると思います。

Hubot はテストコードを書こうとすると自然に読みきってしまえる分量なので、 Hubot 本体の理解のためにテストコードを書くのはオススメです。

```coffeescript
{expect} = require('chai').use(require('sinon-chai'))
{Robot, User} = require 'hubot'
bodyParser = require 'body-parser'
chadventar = require '../'
express = require 'express'
http = require 'http'
path = require 'path'
request = require 'supertest'
sinon = require 'sinon'

describe 'chadventer', ->
  beforeEach (done) ->
    @sinon = sinon.sandbox.create()
    # for warning: possible EventEmitter memory leak detected.
    # process.on 'uncaughtException'
    @sinon.stub process, 'on', -> null

    # start HTTP server (localhost:8888)
    @app = express()
    @app.use bodyParser.json()
    @server = http.createServer(@app)
    @server.listen 8888, =>
      @robot = new Robot(path.resolve(__dirname, '.'), 'shell', true, 'hubot')
      @robot.adapter.on 'connected', =>
        @robot.load path.resolve(__dirname, '../../src/scripts')
        done()
      @robot.run()

  afterEach (done) ->
    # stop server
    @server.close =>
      @robot.brain.on 'close', =>
        close = =>
          @sinon.restore()
          done()
        # NOTE: robot.shutdown() does not close the `robot.server`.
        if @robot.server._handle?
          @robot.server.close close
        else
          close()
      @robot.shutdown()

  describe '#receive', ->
    beforeEach ->
      @robot.adapter.receive = @receive = @sinon.spy()

    it 'works', (done) ->
      request(@robot.server)
        .post '/chadventar/messages'
        .send name: 'bouzuya', text: 'hello'
        .expect 201
        .end (err, res) =>
          expect(@receive).to.have.been.called
          message = @receive.firstCall.args[0]
          expect(message).to.have.property('id', 'messageId')
          expect(message).to.have.property('text', 'hello')
          expect(message).to.have.deep.property('user.id', 'bouzuya')
          expect(message).to.have.deep.property('user.name', 'bouzuya')
          expect(message).to.have.deep.property('user.room', 'room')
          expect(message).to.have.property('room', 'room')
          done(err)

  describe '#send', ->
    it 'works', (done) ->
      name = 'bouzuya'
      text = 'hoge'
      @app.post '/messages', (req, res) ->
        expect(req.body).to.have.deep.equal({ name, text })
        res.sendStatus 201
        done()
      @robot.send new User(name, room: 'room'), text
```

# まとめ

いそぎ足で、ほとんど最小構成の Hubot アダプターをつくってみました。おそらく、お使いのチャットには既に Hubot アダプターはあると思いますが、余裕があれば、自作してみるといいかもしれません。

# 最後に

今日はベイマックス見てきました。だから時間がない、と (ひどい) 。

scoped-http-client は心底ゴミですね。使いづらすぎて笑えるレベル。さっさと投げ捨てて `request` でも載せてくれるとうれしいんですけど。

[getting-started-with-hubot]: https://github.com/github/hubot/tree/v2.9.3/docs#getting-started-with-hubot
[bouzuya/hubot-hello-world]: https://github.com/bouzuya/hubot-hello-world/
[bouzuya/hubot-choice]: https://github.com/bouzuya/hubot-choice/
[bouzuya/hubot-adventar]: https://github.com/bouzuya/hubot-adventar/
[bouzuya/hubot-homepage]: https://github.com/bouzuya/hubot-homepage/
[bouzuya/hubot-script-gist]: https://github.com/bouzuya/hubot-script-gist/
[bouzuya/hubot-brainfxxk]: https://github.com/bouzuya/hubot-brainfxxk/
[bouzuya/hubot-7days-later]: https://github.com/bouzuya/hubot-7days-later/
[bouzuya/hubot-file-brain]: https://github.com/bouzuya/hubot-file-brain/
[hubot-scripts/hubot-redis-brain]: https://github.com/hubot-scripts/hubot-redis-brain
[hubot-script-per-day-61]: http://bouzuya.hatenablog.com/entry/2014/09/12/224215
[github/hubot]: https://github.com/github/hubot/
[github/generator-hubot]: https://github.com/github/generator-hubot/
[ops-for-everyone]: https://speakerdeck.com/johndbritton/ops-for-everyone
[adventar]: http://www.adventar.org/
[express-reference]: http://expressjs.com/3x/api.html
[hubot.github.com]: https://hubot.github.com/
[hubot-adventar-2014]: http://www.adventar.org/calendars/384
[hubot-adventar-2014-bouzuya-index]: http://qiita.com/bouzuya/items/c7d0ad80c357aab6b696
[hubot-adventar-2014-1]: http://qiita.com/bouzuya/items/c7d0ad80c357aab6b696
[hubot-adventar-2014-2]: http://qiita.com/bouzuya/items/11c0c6da2b3ad54b827f
[hubot-adventar-2014-3]: http://qiita.com/bouzuya/items/2a200c9e8a45e2478bc2
[hubot-adventar-2014-4]: http://qiita.com/bouzuya/items/4c0206d72ff22ade9339
[hubot-adventar-2014-5]: http://pasero.net/~mako/blog/s/685
[hubot-adventar-2014-6]: http://qiita.com/bouzuya/items/4e051f68f8d68a2944b0
[hubot-adventar-2014-7]: http://qiita.com/bouzuya/items/2e935c99647b93d8ee1d
[hubot-adventar-2014-8]: http://udzura.hatenablog.jp/entry/2014/12/08/182744
[hubot-adventar-2014-9]: http://qiita.com/bouzuya/items/587c4e920151342e6f1d
[hubot-adventar-2014-10]: http://qiita.com/bouzuya/items/8e8ee8e2e8f83513ad35
[hubot-adventar-2014-11]: http://qiita.com/bouzuya/items/3e239bc530e7c2c3a26c
[hubot-adventar-2014-12]: http://qiita.com/hiconyan/items/3e5481a61657890624dc
[hubot-adventar-2014-13]: http://pasero.net/~mako/blog/s/689
[hubot-adventar-2014-16]: http://qiita.com/bouzuya/items/d65a394cac9e76b56d3d
[hubot-adventar-2014-17]: http://qiita.com/hiconyan/items/baf6ac56129a26d8ac0c
[hubot-adventar-2014-18]: http://qiita.com/bouzuya/items/d5fd0ef9a8cb100933f4
[hubot-adventar-2014-19]: http://qiita.com/bouzuya/items/ae1cb5610a64794bc522
[hubot-adventar-2014-20]: http://bochibochibocchi0095.hatenablog.com/entry/2014/12/21/000710
[hubot-adventar-2014-21]: http://qiita.com/bouzuya/items/ca69548c813e87c2845a
[hubot-adventar-2014-22]: 
[hubot-adventar-2014-23]: http://qiita.com/bouzuya/items/4a6bad68b4b02c17079e
[hubot-adventar-2014-24]: 
[hubot-adventar-2014-25]: