Hubot、前職から色々試して運用してきたが、1年半ぐらいいろいろいじくってるのもあってそろそろ一旦まとめてみたい感じ。
Hubotに関しては hubot/docs at master · github/hubot · GitHub あたりは一通り斜めに読んだが、「僕はこう思ったッス」ぐらいな感じでやっているコードや運用が多いので、適宜マサカリングしていただければと。
便利スクリプトのご紹介
とりあえずこんなの書いた自慢から。
リンク自動で取るやつ
request = require('request') cheerio = require('cheerio') URI = require('URIjs') module.exports = (robot) -> getTitle = (msg, uri) -> request uri, (err, response, html) -> if (err) msg.send "Failed to fetch #{uri}..." return $ = cheerio.load(html) if title = $('title').text().replace(/[\r\n]/g, " ") msg.send "#{title} - #{uri}" else msg.send "Untitled - #{uri}" robot.hear /https?:\/\//i, (msg) -> body = msg.message.text URI.withinString body, (uri) -> robot.logger.debug "URL found: #{uri}" if /\.(jpg|png|gif)$/.test uri # Ignore image else getTitle(msg, uri)
URLっぽい文字列を雑に取得してループを回し、内容を取ってくる。
あと、実際にはtwitterのときはAPI叩いてツイートを引用するとか、GitHubのときは認証がいるのでAPI経由に変えるとか、URLに正規表現を食わせて振り分けている。
ちなみに、エンコーディング問題は対応していない...iconvを噛ませればいいだろうけど。
あと、このスクリプトのアイデアは元々某A社時代の同僚の作ったスクリプトから拝借している。その同僚にはいろいろなことを学んだが、いつか語る日が来るのだろうか...。
ツイート取ってくる
社内IRCにひさいちのツイートを流すbotが作成され、プルリクまで行われており、更にはそのレビューをしろと脅されていますが、僕は元気です。
— ひさいち (@hisaichi5518) 2014, 10月 29
ひさいちさんっていう知らない人のツイットがIRCに勝手に流れてくるようになった
— 、 (@naomeme) 2014, 10月 29
node-cronを用いて定期的にツイーヨを取るようにしたらややウケした。
TweetStalker = require('../lib/tweet_stalker') strftime = require('strftime') class Hisaichi5518Stalker extends TweetStalker screenName: 'hisaichi5518' roomName: '#hisaichi' cronTime: '0 */5 9-19 * * *' # 一度に3つ程度になるよう、適当にツイートを間引く filterEachTweet: (tweet, metadata) -> Math.random() < ( 3 / metadata.tweetSize ) onNewTweet: (t, m) -> url = "https://twitter.com/#{@screenName}/status/#{t.id_str}" date = strftime("%Y-%m-%d %H:%M", new Date(t.created_at)) @send "#{t.text.replace(/\n/, ' ')} [#{date}] - #{url}" onStart: -> @join() onEmpty: -> @send "ひさいち氏、まじめにグロースハックしてるようです" onDone: -> @part()
TweetStalker クラスの実装は省略。そのうちプラグインにするかも... 要るのか...?
APIからどんなものか想像してほしい。イベント駆動な感じで作ったら存外便利に使えるようになった。
スクリプトの開発について
次は色々書いて思ったことを。
デバッグログは多めに仕込むと吉
Hubot Script、とにかくデバッグがつらい...。簡単なtypoでも発見しづらく、黙って死ぬとかしょっちゅう起こる。なので、細かくデバッグログを仕込んで! めっちゃ仕込んで!
request uri, (err, response, data) -> robot.logger.debug "data is: #{data}" robot.logger.debug "err is: #{err}" # ....
実際、Hubotのログレベルのデフォルトは info なので、
$ HUBOT_LOG_LEVEL=debug ./bin/hubot
開発中はこんな感じで。
ログレベルがdebugのログは本番のログを邪魔することは無いし、仕込みまくって最後にしぼったりすれば良いんじゃないかな。 console.log はやめとけ 。約束だよ。
twitterとかgithubとか連携したいじゃん?
scripts/001_twitter.coffee だの scripts/002_facebook.coffee だの先に読まれるっぽい名前でスクリプトを定義する。
で、 robot オブジェクト自体を拡張すると凄い便利。
Twitter = require('twitter') ck = process.env.TWITTER_CONSUMER_KEY cs = process.env.TWITTER_CONSUMER_SECRET at = process.env.TWITTER_ACCESS_TOKEN as = process.env.TWITTER_ACCESS_SECRET module.exports = (robot) -> robot.twitter = new Twitter(consumer_key: ck, consumer_secret: cs, access_token_key: at, access_token_secret: as) robot.logger.info "Setting up twitter API..."
で、自分たちのスクリプトの中でこんなんする。
robot.twitter.get "/statuses/show/#{statusId}.json", include_entities: true, (data) -> msg.send "#{data.user.screen_name}: #{data.text}"
毎回コンシューマキーが云々をしなくていい、楽!!!!
オープンにソーシャルとマッシュアップするとだいぶHubot 2.0と言う感じになってくる。
あ、でもこれってもしかしてプラグインとかあるのかもしれない...ですね...。
CoffeeScriptらしいインターフェースを考える
なにかIOが発生するとすべてコールバックな世界で、色々な失敗を経て、失敗したときとか成功したときとかいろんなコールバックを渡すと便利と言う結論に至った。
getMessages = ({onNewTweet, onEmpty, onError}) -> lastId = getLastId() robot.logger.debug "Last update hisaichi tweet id is: #{lastId}" twCallback = (data) -> if data instanceof Error data.message ?= data.data return onError(data) tweeted = 0 for tweet in data if lastId >= tweet.id robot.logger.debug "No new tweet. Skip." break else if tweet.retweeted_status? robot.logger.debug "RT should be skipped" else onNewTweet(tweet) tweeted++ onEmpty() unless tweeted robot.brain.data.lastId = lastId = data[0].id try robot.twitter.get "/statuses/user_timeline.json", {screen_name: 'hisaichi5518', count: 15}, twCallback catch e onError(e) robot.respond /hisaichi/i, (msg) -> getMessages onNewTweet: (t) -> msg.send "[debug] OK: #{t.text.replace(/\n/, ' ')}" onEmpty: -> msg.send "[debug] No new tweet..." onError: (e) -> msg.send "[debug] NG: #{e}"
ひさいち君のツイートを取得するスクリプト、プロトタイプ版はこんな感じだった。コールバックはただの引数より名前付き引数として渡せた方が、CoffeeScript的には格好良くなると思う。
coffee コマンド、便利
Hubot起動時、スクリプトのコンパイルエラーをした場合エラーメッセージはでるけど、どの行でエラーになったとかがよくわからなくてつらい感じがあった。 coffee -p scripts/hoge.coffee ってやればコンパイルエラーの場所が分かる。
さらに言うとコンパイルされた結果も分かるので、「Coffeeの細かいイディオムわからんちん」と言うときでもJavaScript変換後の姿を追いかけてデバッグできるので良い。
IRC 固有の機能を使う
知られている気もするがギョームではIRCを良く使っている。なので、HubotとはいえIRCっぽい機能が使えた方が便利だったりする。
Hubotのアダプターのインスタンスは robot.adapter に格納されており、 hubot-irc アダプターが実際にIRCとやり取りしている箇所はrobot.adapter.botでアクセスできる。なので、 List of Internet Relay Chat commands - Wikipedia, the free encyclopedia にあるようなコマンドを直接発行することができる。
以下はIRCの「なると」を与えるためのスクリプト(Hubot自身に管理権限が要る)。
module.exports = (robot) -> giveNaruto = (msg, name) -> if robot.adapterName == 'irc' robot.logger.info "Giving naruto:", msg.envelope.room, name robot.adapter.bot.send 'MODE', msg.envelope.room, '+o', name msg.send "Naruto is successfully given for @#{name}" else msg.send "Naruto plugin is unavailable for #{robot.adapterName}" robot.respond /naruto me/i, (msg) -> giveNaruto msg, msg.envelope.user.name robot.respond /naruto give ((?:[-_a-zA-Z0-9]+\s*)+)/i, (msg) -> names = msg.match[1].split(/\s+/) for name in names giveNaruto msg, name
JOIN、PARTなんかも使える。ikachan並みの機能を持たせることも可能。
なお、こういうアダプタ固有の機能を使うとき、デバッグ時のShell modeでめんどくさい目に遭う(黙って落ちるとか)こともあるため、robot.adapterNameを見て適切に分岐させるのがいいと思う。
テストを書く?書かない?
最近、 Testable Hubot - TDDでテストを書きながらbotを作る と言う記事がバズった。これはこれで良いものだと思う。しかし正直僕はテスト、あまり書けていない...。
Hubot スクリプトのテストとか書いてもたいてい書き捨てるだけだから、書かなくていいよ。
— bouzuya (@bouzuya) 2014, 10月 29
むしろ、テストほしいと思うくらいでかいなら、別途ライブラリとかつくって、そっちをテストしろよって思ってしまう。
— bouzuya (@bouzuya) 2014, 10月 29
この辺のご意見非常に分かる(分かる、とかってにぼくが思ってるだけかも)。
例えば、内部でクラス作ってしまった!!! レベルであれば、それは書かないと怖いですね... と思う。 hubot-mock-adapter と、あとsinonの時間操作するやつとか、 pgte/nock · GitHub とか便利なんじゃないでしょうか。
まあ、Yakはいっぱいいると思うな...。
Hubot を運用することについて
主に「どこにデプロイすんの?」という話
色々派閥がある気がする。
- supervisord
- Heroku
- Dokku
有名かもしれないがHubotはプロジェクトを生成した最初の状態で、いきなりHerokuにプッシュすれば動くようになっている。なのでsupervisordとかにすると、やれnodeのインストールだ、やれキャピストラーノだ、と手間が多い印象がある。git pushでデプロイできるのは非常に楽で、おすすめできる。
ただまあ、Herokuからだと社内のGithubエンタープライズ見えないんす、とかあると思うので、あえて progrium/dokku · GitHub とかをお勧めする。
Hubotのプロジェクトは、Dokkuにプッシュしても同じようにすぐ動く。Dokku、経験では、ウェブアプリをホストするのには正直色々きついが、Hubot程度であれば割と安定してホスト出来る。
ただまあ、情報が少ないのは間違いないと思う。公式ドキュメントだけで自力でRedis Plugin入れられる程度に頑張れるなら、トライしてみて良いと思う。
所感
ここに書いた他にも、
- httping でホストの状態を確認してくれる君(内部でexecしてる)
- 指定したホストのmuninのURL出してくれる君
など、ギョームをちょっとだけ楽にしてくれる便利なスクリプトをみんなが書いている。
だが、ぼくは見方によってはどうしょうもない、それこそ書き捨てだったり、三日で飽きられるようなスクリプトも思いついては書いている。
思うに、ボットが面白いのって機能的なところより、なんか変な機能があったり、単純なプログラムで動いてるはずなのに妙にやり取りが面白かったり、と言った、文化的な側面の方が大きいんじゃないだろうか、と思っている。
チャットの窓というのはチームのみんなの生活の場の一部だと思うんだけど、Hubotの導入は、場に対して面白ロボットを仲間に加えるイメージを持っている。なんでまあ、Hubotじゃなくていいんだけど、Hubotは:
- なんだかんだ情報が多い
- アダプターのおかげで、基本的には特定のチャットアプリケーションに依存せず移行できる
- CoffeeScriptが楽しければ楽しいし、あとnode.jsなんでなんだかんだ速い
と言ったあたりが有利じゃないかと思っている。
文化の話に戻ると、経験上でも、便利情報を出すやつよりも占いとかクイズの問題を出す機能の方が毎日使われたりするわけで...。もちろんついでにデプロイするとかモニタするとか便利機能があれば使うと思うんだけど。
で、正直、ChatOpsなんてバズワードしゃらくせえや...、という感想を持っている。それよりクールなボットの機能を書いて思った通りに動いたり、作ったスクリプトが身内に受けたりするのが楽しいので、そういうのをモチベーションにスクリプトを書いてる気がする。あとチームみんなで機能をどんどん足してカオスになったりするとことか。
もう一点、単純にCoffeeScriptは楽しいとも思う。使う頭の箇所が普段のRubyとかと割と違うので...良い。
これでおしまい
そんな感じでまあ、「Hubot使ってはいるけどこれって合ってるの?」みたいなことを思っているぼくのようなプログラマのため、何かの参考になればと思う。