Hubot

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

More than 3 years have passed since last update.

これは Hubot Advent Calendar 2014 の 24 日目の記事です。

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


前回まで、そして今回は

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

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


Chadventar という仮想チャット

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

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

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

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

では、見てみましょう。


サンプルコード


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

module.exports.use = (robot) ->

new Chadventar(robot)

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


Chadventar#run

  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

  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 を使って、Chadventar サーバーにメッセージを送信しています。


テストコード

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

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

{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 でも載せてくれるとうれしいんですけど。

[hubot-adventar-2014-22]: