hubot adapterの作り方
はじめに
最近chatworkやtypetalkといったコミュニケーションツールのhubot adapterを作ったりしてます。
どちらもまだ開発途中ですが、これまでに得た知見を残していこうと思います。
間違った情報があったらコメント等でご指摘いただければ嬉しいです。
hubotとは
hubotはgithub社が開発しているchat用のbotフレームワークです。
通常のbotとどう違うのかというと、botのロジックとchatとの処理部分を分離して、様々なサービスへ応用できるようにしているところです。
前者のbotのロジック部分をhubot scriptと呼び、後者のchatとの処理部分をhubot adapterと呼びます。
hubot scriptには例えば天気を教えてくれるものからjenkinsにbuildを行わせるものまであります。 もちろん、自分で開発して追加することも簡単にできます。scriptは開発例なども豊富でわかりやすいです。
対してhubot adapterは各種サービスとのやりとりを行うものです。IRC用はもちろん、twitter用やyammer用などさまざまなadapterが公開されています。今回説明するのはこのadapterの開発方法です。
hubotについてもっと詳しく知りたい方や、hubotの導入方法などは、こちらの資料が非常にわかりやすいです。
hubotのアーキテクチャ
矢印の説明とかなんかいろいろ怪しいですが、だいたいこんなイメージです。
この図のうち、真ん中の青いところがhubotのコア部分となっており、scriptとadapterとの橋渡しや便利機能の提供をしています。
adapterの役割は、サービスからデータを受け取り(run)、そのデータをrobotというモジュールに渡し(receive)、hubot scriptから受け取ったメッセージをサービスに渡すこと(send/reply)です。
公式ドキュメント
adapterの実装方法を知るには公式ドキュメントが一番です。 この記事を書いている時点で、ドキュメントには以下の説明があります。
The best place to start is
src/adapter.coffee
, and inheriting fromAdapter
. There is not as much documentation as could exist (yet!), so it is worth reviewing existing adapters as well as how hubot internally uses an adapter.(意訳: まず
src/adapter.coffee
を読め。あ、まだドキュメントはねーから。あとはコード読んで頑張れよ!)
というわけでコードを読みましょう!!
というだけなのもアレなので、すこし補足したいと思います。
adapterのコード
実際にhubotのsrc/adapter.coffeeを見てみると、Adapter
というクラスがあります。
これがadapterのベースとなります。
実装時にはAdapter
を継承したクラスを作り、必要なメソッドをオーバーライドしていくことになります。
その中でも重要なメソッドはsend
とrun
です。
send
これはhubot scriptがなんかの処理を行ったあと、チャット側になにかを通知するときに利用するメソッドで、scriptから呼ばれるメソッドの中では一番利用頻度の高いものです。
その他のメソッドとしてはemote
やreply
、topic
、play
など色々ありますが、これらはsend
と同様にチャット側に通知するのが目的です。これらは必要に応じて実装すればよいと思います。
例えばplay
は定義のみの空メソッドですが、音の再生が可能なチャットサービスは限られているので、他のhubot adapterを見ても実装していないことが多いです。
run
hubotのコード上ではチャットとやり取りをするもののことをbot
と呼んでいます(hubotだのrobotだの色々出てきてややこしいですね)。
run
はこのbot
の初期化と、チャットから継続的にメッセージを取得し発言者情報とともに@receive
にデータを渡すための処理を実装します。
Hoge adapterの解説
実際のコードに近いものを、Hogeという架空のチャットサービスのためのadapterという形で解説したいと思います。
以下はhubot公式のadapterであるhubot-campfire
から重要な部分を抜粋して書き直したものです。
{Adapter, TextMessage} = require 'hubot' {EventEmitter} = require 'events' class Hoge extends Adapter send: (envelope, strings...) -> @bot.send str for str in strings run: -> options = token: process.env.HUBOT_HOGE_TOKEN rooms: process.env.HUBOT_HOGE_ROOMS account: process.env.HUBOT_HOGE_ACCOUNT bot = new HogeStreaming options, @robot bot.on 'message', (userId, userData, message) -> user = @robot.brain.userForId userId, userData @receive new TextMessage user, message bot.listen() exports.use = (robot) -> new Hoge robot class HogeStreaming extends EventEmitter constructor: (options, @robot) -> @token = options.token @rooms = options.rooms.split(",") @account = options.account send: (message) -> # チャットにメッセージを送信する処理... listen: -> # チャットから継続的にメッセージを取得する処理 # メッセージを取得したら... # @emit 'message', user, message
Hogeクラス
Hoge
はAdapter
を継承しているクラスで、メインの処理を記述しています。
前述のsend
とrun
というメソッドをオーバーライドしています。
send
send
は単にHogeStreaming#send
を呼び出しているだけです。
ここでenvelope
にはユーザ情報やルーム情報が格納されています。
run
run
はまず環境変数の読み取りを行っています。
hubotは設定を環境変数で行う設計になっているので、慣習的にアクセストークンや参加するルーム情報などを環境変数から受け取ります。
次にHogeStreaming
の初期化とチャットサービスメッセージ受け取りを行っています。
メッセージを受け取ったら@robot.brain.userForId
でユーザインスタンスを作り、ユーザインスタンスと取得したメッセージからTextMessage
を作成し、それを@receive
に渡しています。
@receive
にデータを渡すことで、各種hubot scriptが発火する仕組みになっています。
TextMessage
以外にもいくつかメッセージの種類が定義されているので、使い分けると良さそうです。
HogeStreamingクラス
HogeStreaming
はhubotが呼ぶところのbot
にあたるもので、チャットのAPIを叩くなどをしてメッセージの取得や送信を行うクラスです。
メッセージの取得時にはユーザ情報を付与してHoge
に渡しています。
Hoge adapterの利用
実際に利用する際には、hubot-hoge
という名前のnpmパッケージをnpm install
するか、src/robot.coffee
のHUBOT_DEFAULT_ADAPTERS
に'hoge'
を追加し、以下のコマンドでhubotを立ち上げます。
bin/hubot -a hoge
その他参考になるコード
shell & campfire
どちらも公式のadapterです。基本的な流れをつかむには一番わかりやすいと思います。
hubot-twitterは一番参考にされているadapterだと思います。 基本的な実装以外でも、OAuthやストリーミングAPIを利用するときに参考になりそうです。
その他
yammerはcometを、hipchatはXMPP(jabbar)を、idobataはpusherを使っているため、それぞれ似たようなサービスの実装時に参考になると思います。
- https://github.com/athieriot/hubot-yammer
- https://github.com/hipchat/hubot-hipchat
- https://github.com/idobata/hubot-idobata
まとめ
書く気力が尽きました…。
実装の雰囲気が伝わったかどうかわかりませんが、hubotならこんな感じで簡単にbotが作れます。 ぜひお試しください!
google spreadsheetの変更をhubotに通知する
スプレッドシートの値が変更されたら、その変更された内容をhubotに渡して投稿する仕組みを考えてみます。
hubot側
hubot scriptでは簡単にhttpリクエストを処理できるので、それを使って通知を受け取るようにします。
google docsやspreadsheet用のscriptはさすがになさそうだったので、以下のscriptを参考に書いてみました。
参考
https://github.com/github/hubot-scripts/blob/master/src/scripts/github-pull-request-notifier.coffee
# Dependencies: # "url": "" # "querystring": "" # # Commands: # None # # URLS: # POST /hubot/google-spreadsheet?room=<room>&name=<name>&value=<value> # url = require 'url' querystring = require 'querystring' module.exports = (robot) -> robot.router.post "/hubot/google-spreadsheet", (req, res) -> query = querystring.parse (url.parse req.url).query res.end() return unless query.room user = room: query.room name = query.name or "未設定" value = query.value or "未設定" try message = "【#{name}】の値が#{value}に変更されました。" robot.send user, message console.log message catch error console.log "google spreadsheet notifier error: #{error}. Request: #{req.body}"
google spreadsheet側
まず、変更を検知したいspeadsheetを開き、メニューの「ツール > スクリプト エディタ」を選択します。
変更を処理するスクリプトはgoogle apps script(GAS)というjs拡張言語で記述していきます。
以下の実装ではC列が変更されたとき、F列とC列の値をそれぞれname
とvalue
としてhubotに送っています。
このスクリプトを使うときにはroom
の値やhubotのホスト名を適切な値に変更してください。
function postToHubot(event) { if (!event) { return; } var range = event.source.getActiveRange(); var col = range.getColumn(); var row = range.getRow(); // C列が変更されたとき if (col == 3.0) { try { var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheets()[0]; var room = 1234; var value = event.value; var name = sheet.getRange(row, 6.0).getValue(); var params = '?room=' + room + '&name=' + name + '&value=' + value; var res = UrlFetchApp.fetch( 'http://hubot.example.com:8080/hubot/google-spreadsheet' + params, { 'method': 'POST' }); } catch(err) { Logger.log(err); } } }
後はスプレッドシートの値が変更されたときにスクリプトの関数が実行されるようにスクリプトエディタのメニューで「リソース > 現在のプロジェクトのトリガー」を選択し以下のように設定します。
これで編集するたびにこの関数が呼ばれるようになります。
ハマったところ
gasを使うのは初めてだったのですが、以下の2点でハマりました。
onEdit()のパーミッション
最初はonEdit()内に通知の処理を書いていたのですが、UrlFetchApp#fetch
で例外が出て動きませんでした。
原因はonEdit()のパーミッションで、自作関数をトリガーに登録する場合はユーザ権限で実行になるため問題ないのですが、onEdit()はシステムの権限で実行されるようで例外が出ていました。
例外
そもそも最初はこの例外が出ていることに気がつかず(例外が出てもログになにも出ないため)、気づくのに時間がかかりました…。
感想とか
やっぱりhubot scriptは簡単に書けて良いですね。
gasはちょっと書くのがめんどくさいですが(coffeescriptじゃないしドキュメントもわかりにくい)、spreadsheet以外でも色々できそうなので通知書くと便利な場面は多そうだなと思いました。
mocha+chai+blanketを使ってcoffeescriptでBDD
最近空いた時間にhubotのadapterを開発してるのですが、coffeescriptでテストを書きたくなったときにちょっと試行錯誤したので備忘録として残しておきます。
目標
- mochaとchaiを使ってcoffeescriptでBDDスタイルのテストを行う
- blanketを使ってcoffeescriptのカバレッジ測定を行う
- (おまけ) travis ciとcoverallsで自動化する
mochaとchaiでBDD
セットアップ
mochaはjs用のテストフレームワークで、C/Sのどちらでも動作します。他のアサーションライブラリと組み合わせて使う必要があるため、今回はBDDアサーションライブラリであるchaiを使います。
まずpackage.json
に以下を追記します。
{ "scripts": { "test": "mocha test" } }
次に必要なmoduleをインストールします。
npm install coffee-script -g npm install chai --save-dev npm install mocha --save-dev
次にtest/mocha.opts
というファイルを作ります。
これはmochaに渡すコマンドライン引数をまとめたものです。
直接引数として渡しても問題ないのですが、長くなりがちなのでこっちの方がのちのち便利かな思います。
--compilers coffee:coffee-script
テストコード
chaiにはTDDスタイルのassertを使う方法と、BDDスタイルのshould/expectを使う方法があります。 個人的にはshouldスタイルが好きなので以下はshoudスタイルの例。
# test/sample.test.coffee should = (require 'chai').should() describe 'String', -> describe '#concat()', -> it 'should return "John Doe"', -> # assert values 'foo'.concat('bar').should.equal 'foobar' describe '#split()', -> it 'should return [1,2,3]', -> # assert arrays and objects 'foo,bar,baz'.split(',').should.deep.equal ['foo', 'bar', 'baz'] describe 'Tweet', -> # async test it 'should post', (done) -> tweet = new Tweet 'Hello!' tweet.post 'Hello!', (err, res) -> done()
その他いろいろな使い方はAPIリファレンスを参照。
sinonと組み合わせて使うのも簡単です。
ちなみに上に書いたコードの中でdescribe
やit
といった枠組みを提供しているのがmochaです。before
やafter
などもあります。
テストの実行
npm test
blanketを使ったカバレッジ測定
もともとcoverallsでカバレッジ測定したかったので、以下の要件でライブラリを探してました。
- node-coveralls対応
- coffeescript対応
- mocha対応
そこで試したのがibrik(coffeescript用istanbul)とblanketだったのですが、ibrikの方はパーサーがエラーで動かず。 結局blanketを使うことにしました。
セットアップ
package.json
に以下を追記します。
{ "config": { "blanket": { "pattern": "src", "loader": "./node-loaders/coffee-script", "data-cover-never": "node_modules" } } }
上記のpatternにカバレッジ計測対象のファイルにマッチするパターンを書きます。
このpatternなんですが、上の例だとフルパスにsrc
が含まれてる場合(/Users/akiomi/src/hoge-project/
など)はプロジェクト直下のファイルがすべて含まれてしまったので"pattern": "hoge-project/src"
とするなどちょっと工夫が必要です。
次はblanketのインストール。
npm install blanket@">=1.1.5" --save-dev
blanketの古いバージョン(1.1.4以前)ではpackage.json
のscriptsの中に設定を書かなければいけなかったのですが、新しいnpmを使っているとpublish時にscriptsに文字列以外が含まれているエラーとなってしまいます。
blanketの1.1.5からはconfigに設定を書くことが推奨となりこの問題が解決されたので、npm publish
を行う場合には1.1.5以上を使用した方がよさそうです。
また原因がわからないのですが、travis-ciでビルドする場合には1.1.5を指定してもうまく動かなかったため、1.1.6が出るまではgitのコミットハッシュを指定するなどして対応が必要です。
そしてmochaからblanketを呼び出すために以下をtest/mocha.opts
に追記。
--require blanket
ここで、カバレッジ測定しやすくするためにMakefile
を作ります。
本当はgruntを使いたかったんですが、travis-ciでgrunt-mocha-covを使って動かすことができず断念…(ローカルの実行ではうまくいくんですが)。
MOCHA = ./node_modules/.bin/mocha .PHONY: test test: $(MOCHA) test test-coverage: $(MOCHA) -R html-cov test > coverage.html
カバレッジ計測
以下のコマンドでテストとカバレッジ測定が出来ます。
make test # テスト make test-coverage # カバレッジ計測
あとpackage.json
の方のscripts.testも変更しておきます。
{ "scripts": { "test": "make test" } }
travis-ciとcoveralls
githubでリポジトリを公開している場合はtravis-ciによるビルドとcoverallsによるカバレッジ計測が簡単にできます。 travis-ciとcoverallsの設定は省略。
まずcoverallsでのカバレッジ計測用のmoduleをインストール。
npm install mocha-lcov-reporter --save-dev npm install coveralls --save-dev
次にMakefile
にtravis-ci用のビルドタスクを追加。
test-coveralls: mocha test --reporter mocha-lcov-reporter | coveralls
あとはプロジェクトルートに.travis.yml
を作るだけ。
language: node_js node_js: - "0.10" script: make test-coveralls
これでリポジトリにpushした段階で勝手にtravis-ciがビルドしてくれて、ビルド結果をcoverallsに連携してくれます。キーやトークンの設定とかも特に必要ありません。
感想とか
今回試したいろいろなライブラリは成熟してないものが多くてハマりポイントが多かったです。 全体的にまだいいやり方があると思うので、もうちょっと試行錯誤してみようと思います。
参考になるかはわかりませんが、以下のリポジトリで実践しています。