読者です 読者をやめる 読者になる 読者になる

ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

最近のHubotの運用とか

Hubot、前職から色々試して運用してきたが、1年半ぐらいいろいろいじくってるのもあってそろそろ一旦まとめてみたい感じ。

Hubotに関しては hubot/docs at master · github/hubot · GitHub あたりは一通り斜めに読んだが、「僕はこう思ったッス」ぐらいな感じでやっているコードや運用が多いので、適宜マサカリングしていただければと。

便利スクリプトのご紹介

とりあえずこんなの書いた自慢から。

リンク自動で取るやつ

f:id:udzura:20141031183819p:plain

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社時代の同僚の作ったスクリプトから拝借している。その同僚にはいろいろなことを学んだが、いつか語る日が来るのだろうか...。

ツイート取ってくる

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からどんなものか想像してほしい。イベント駆動な感じで作ったら存外便利に使えるようになった。

困ったらAPIを別途立てて叩く

むかし書いた通り。


In-house Domoraen - killed_by?(Charity)

これ、mecabをnodeから操って同じことをさせるのも手だったけど、なんかファンキーな気がしたのと、Rubyの実装が既にあったのでいいか...と言う感じ。

他にも、WebistranoにAPIを生やして、Hubotからそれを叩いてデプロイやら仕事をさせるスクリプトを書いた人もいる。nodeであらゆるすべてをやってもいいけど状況に応じて一番早い方法を選ぶ方が僕は好き。

スクリプトの開発について

次は色々書いて思ったことを。

デバッグログは多めに仕込むと吉

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-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入れられる程度に頑張れるなら、トライしてみて良いと思う。

監視について

テストも書かず、あまつさえ監視もやってない...なんということだ...

言い訳すると、ボットは普段みんながいるチャンネルに常駐してるんだから、落ちてたら誰か気付くよね...みたいな感じで後回しにしている。

その気になれば Hubot の POST /hubot/ping とかでNagiosを仕掛けられるんじゃないかと思うが、カジュアルにハボットをpushしてる場合は... Serf??? Sensu??? ウームあまり答えを持っていない。

所感

ここに書いた他にも、

  • httping でホストの状態を確認してくれる君(内部でexecしてる)
  • 指定したホストのmuninのURL出してくれる君

など、ギョームをちょっとだけ楽にしてくれる便利なスクリプトをみんなが書いている。

だが、ぼくは見方によってはどうしょうもない、それこそ書き捨てだったり、三日で飽きられるようなスクリプトも思いついては書いている。

思うに、ボットが面白いのって機能的なところより、なんか変な機能があったり、単純なプログラムで動いてるはずなのに妙にやり取りが面白かったり、と言った、文化的な側面の方が大きいんじゃないだろうか、と思っている。

チャットの窓というのはチームのみんなの生活の場の一部だと思うんだけど、Hubotの導入は、場に対して面白ロボットを仲間に加えるイメージを持っている。なんでまあ、Hubotじゃなくていいんだけど、Hubotは:

  • なんだかんだ情報が多い
  • アダプターのおかげで、基本的には特定のチャットアプリケーションに依存せず移行できる
  • CoffeeScriptが楽しければ楽しいし、あとnode.jsなんでなんだかんだ速い

と言ったあたりが有利じゃないかと思っている。

文化の話に戻ると、経験上でも、便利情報を出すやつよりも占いとかクイズの問題を出す機能の方が毎日使われたりするわけで...。もちろんついでにデプロイするとかモニタするとか便利機能があれば使うと思うんだけど。

で、正直、ChatOpsなんてバズワードしゃらくせえや...、という感想を持っている。それよりクールなボットの機能を書いて思った通りに動いたり、作ったスクリプトが身内に受けたりするのが楽しいので、そういうのをモチベーションにスクリプトを書いてる気がする。あとチームみんなで機能をどんどん足してカオスになったりするとことか。

もう一点、単純にCoffeeScriptは楽しいとも思う。使う頭の箇所が普段のRubyとかと割と違うので...良い。

これでおしまい

そんな感じでまあ、「Hubot使ってはいるけどこれって合ってるの?」みたいなことを思っているぼくのようなプログラマのため、何かの参考になればと思う。