これは Hubot Advent Calendar 2014 の 24 日目の記事です。
また、今回は @bouzuya の Hubot 連載の第 16 回です。目次は、第 1 回の記事にあるので、そちらをどうぞ。
前回まで、そして今回は
前回は [Hubot 標準同梱の Shell アダプターを読んでみよう][hubot-adventar-2014-23] ということで、Hubot アダプターについて軽く紹介し、コードを読みました。
今回は Hubot アダプターを実際につくってみましょう。
Chadventar という仮想チャット
今回のために bouzuya/hubot-chadventar というアダプターをつくりました。動くかは分かりません。今回のためにつくった仮想チャットのための Hubot アダプターなので。
今回、想定しているのは Chadventar というチャットです。
POST /messages
に { name: ..., text: ... }
で JSON
を受け付けて、受信したメッセージをチャットに流します。
チャットに流れているメッセージは指定した URL (今回なら Hubot の /chadventar/messages
) に POST
メソッドで { name: ..., text: ... }
の JSON
で送られます。
では、見てみましょう。
サンプルコード
{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]:
[hubot-adventar-2014-23]: http://qiita.com/bouzuya/items/4a6bad68b4b02c17079e
[hubot-adventar-2014-24]:
[hubot-adventar-2014-25]: