GitHub社謹製! bot開発・実行フレームワーク「Hubot」

第5回実用的なHubotのスクリプトを書いてみる

第4回までで、Hubotをセットアップしてチャットツールに接続し、独自のスクリプトを書くことができるようになりました。最終回となる今回は、もう少し複雑なスクリプトの書き方をサンプルコードをベースに紹介します。

定期実行で定時ミーティングの時間を通知する

デイリースクラムの時間を通知すると言った定期的な処理をHubotに行わせたい場合、cronモジュールを使うと便利です。本節では、cronモジュールの導入方法と使用例のサンプルスクリプトを掲載します。

cronモジュールの導入

cronモジュールのようなnpmのモジュールを使用するには、Hubotを導入したディレクトリの直下にあるpackage.jsonファイルのdependenciesにモジュールの情報を追加します。

通常、Hubotをインストールした直後はdependenciesの項目は次のようになっています。

  "dependencies": {
    "hubot":         ">= 2.6.0 < 3.0.0",
    "hubot-scripts": ">= 2.5.0 < 3.0.0"
  },

ここにcronモジュールを追加することでスクリプトからcronモジュールを使用できるようになります。

  "dependencies": {
    "hubot": ">= 2.6.0 < 3.0.0",
    "hubot-scripts": ">= 2.5.0 < 3.0.0",
    "cron": ">= 1.0.4"
  },

サンプルスクリプト

cronモジュールを使って月曜日から金曜日の午前11時にミーティングの開催を呼びかけるスクリプトの例を示します。

cronJob = require('cron').CronJob

module.exports = (robot) ->
  cronjob = new cronJob('0 0 11 * * 1-5', () =>
    envelope = room: "#chatroom"
    robot.send envelope, "デイリースクラムを始めましょう @all"
  )
  cronjob.start()
図1 cronを使ったサンプルスクリプトの実行例
図1 cronを使ったサンプルスクリプトの実行例

見てのとおり、cronモジュールでは、通常のcronと同様の書式で定期処理を書くことができます。

注意点として、普通のhearrespondを使った発言方法ではmsgオブジェクトのsendメソッドなどを使いますが、定期実行のように誰かから話しかけられて返答するのではない場合は、robotオブジェクトのsendメソッドを直接使ってチャットルームに発言することになります。

sendメソッドの第一引数envelopeは発言先のチャットルームや発言対象のユーザの情報を格納したオブジェクトです。この例ではhubot-ircを使ってIRCの#chatroomチャンネルに発言することを想定しています。どのようなenvelopeが必要かは、使用しているAdapterよって異なるため注意が必要です。

cronモジュールの詳しい使い方はリファレンスを参照してください。

永続化機能で投票結果を保存する

Hubotに何かを記憶させようとしたとき、初期状態ではメモリ上に情報を置くことしかできません。そのため、Hubotを再起動すると情報が消えてしまいます。この問題に対して、Hubotではbrainというインターフェースを通じて様々なストレージに情報を永続化できます。

本節では、Redisを使った永続化の方法と、チャット参加者に投票する仕組みを作る例を記します。

redis-brainによるRedis永続化機能の追加

Redisを使った永続化を行うには、HubotからアクセスできるところでRedisが動いている必要があります。Redisの公式サイトなどを参考に予めRedisをインストールして、起動してください。

HubotとRedisを接続するには、hubot-scriptsredis-brain.coffeeを使う方法が簡単です。redis-brain.coffeeを有効化するためには、hubot-scripts.jsonredis-brain.coffeeを読み込み、環境変数で接続先のRedisインスタンスを指定します。

hubot-scripts.json
["redis-brain.coffee"]
run_hubot.sh
#!/bin/bash

# redis://<host>:<port>[/<brain_prefix>]
export REDIS_URL=redis://127.0.0.1:6379/hubot

bin/hubot

このように設定すると、Hubotのbrainから保存したデータを127.0.0.1:6379のRedisにhubot:storageというキーでJSON文字列として保存するようになります。

サンプルスクリプト

brainを使った永続化のサンプルとして、チャット上で何か良いことをしたメンバーに投票する仕組みを作ってみます。

module.exports = (robot) ->
  KEY_SCORE = 'key_score'

  getScores = () ->
    return robot.brain.get(KEY_SCORE) or {}

  changeScore = (name, diff) ->
    source = getScores()
    score = source[name] or 0
    new_score = score + diff
    source[name] = new_score

    robot.brain.set KEY_SCORE, source
    return new_score

  robot.respond /list/i, (msg) ->
    source = getScores()
    console.log source
    for name, score of source
      msg.send "#{name}: #{score}"

  robot.hear /^(.+)\+\+$/i, (msg) ->
    name = msg.match[1]
    new_score = changeScore(name, 1)
    msg.send "#{name}: #{new_score}"

  robot.hear /^(.+)--$/i, (msg) ->
    name = msg.match[1]
    new_score = changeScore(name, -1)
    msg.send "#{name}: #{new_score}"
図2 永続化機能を使ったサンプルスクリプトの実行例
図2 永続化機能を使ったサンプルスクリプトの実行例

brainの詳細については、公式のリファレンスソースコードを参照してみて下さい。

HTTPリクエストを送ってURLからコンテンツを取得する

本節では、Hubotに組み込まれているscoped-http-clientを使用してHTTPリクエストを送る方法について説明します。

scoped-http-clientの使用方法

Hubotには、RobotクラスのオブジェクトとResponseクラスのオブジェクトにscoped-http-clientのオブジェクトがセットされているため、単純にGETPOSTでHTTPリクエストを送って結果を取得するだけなら新たなモジュールを追加する必要はありません。

URLを受け取って取得したHTML文字列をチャットに発言するサンプルを次に示します。

module.exports = (robot) ->
  robot.respond /get (.*)/i, (msg) ->
    url = msg.match[1]
    msg.http(url).get() (err, res, body) ->
      msg.send body
図3 HTTPリクエストを送信するサンプルスクリプトの実行例
図3 HTTPリクエストを送信するサンプルスクリプトの実行例

msg.httpにURLを渡すと、HTTPリクエストを送るためのオブジェクトが返ってきます。このオブジェクトの実体は、scoped-http-clientのScopedClientオブジェクトです。

scoped-http-clientの詳しい使い方は公式リファレンスを参照してください。

サンプルスクリプト

ここでは、例としてチャットにURLが投稿されると、そのページのタイトルを取得してHubotが発言するというスクリプトを紹介します。

Node.jsでスクレイピングするときには、jQueryに似た書式で書けるcheerioモジュールを使うと便利です。cheerioを使うには、cronモジュールのときと同様にdependenciesに追加してください。

package.json
"cheerio": ">= 0.17.0"
cheerio = require 'cheerio'

module.exports = (robot) ->
  robot.hear /(https?:\/\/.*)/i, (msg) ->
    url = msg.match[1]
    msg.http(url).get() (err, res, body) ->
      $ = cheerio.load(body)
      title = $('title').text()
      msg.send title
図4 titleタグを抽出するサンプルスクリプトの実行例
図4 titleタグを抽出するサンプルスクリプトの実行例

このように、HTTPリクエストを送って結果を解析することは簡単に行えます。WebAPIを叩くような処理も同様に実装できます。

本節で紹介したscoped-http-clientはモジュールを追加しなくても使えるため手軽ではありますが、302リダイレクトのレスポンスが返ってきた場合に自動で遷移先のコンテンツを取得してくれなかったりと不便な点もあります。実装したい機能をscoped-http-clientモジュールで実現できない場合は、requestなど別のモジュールを使用することも検討して下さい。

Webサーバ機能を使って外部からのリクエストを待ち受ける

HubotはデフォルトでexpressによるWebサーバが立ち上がるようになっており、スクリプトからルートを登録することで特定のURLに対するリクエストを受け取り処理することができます。 この機能を使うと、簡単に外部からHubotに対して情報を渡してアクションを起こさせることができます。

注意点として、HubotのWebサーバを直接インターネットに晒してリクエストを受け付けると、外部から誰でもHubotにリクエストを送ることができるためセキュリティ上の問題があります。ローカルネットワークからのみリクエストを受け付けるようにするか、外部に対して開く場合は別途認証や検証の仕組みを入れることを検討してください。

本節では、Webサーバ機能の概要と使用例を掲載します。

Webサーバ機能について

Hubotを起動すると、とくに何も設定しなくてもWebサーバ機能が有効になっており、8080番ポートでリクエストを待ち受けている状態になります。しかし、expressにルートを登録していないとリクエストを送っても何も反応しません。

hubot --createしたときに最初から使用可能になっているhttpd.coffeeスクリプトを使うと、Webサーバ機能の動作を簡単に確認することができます。このスクリプトがscriptsディレクトリに配置してある状態でHubotを普通に起動すると、/hubot/version, /hubot/ping, /hubot/time, /hubot/info, /hubot/ipのルートが登録されます。

Hubotを起動した状態で、同じサーバ上で次のようにリクエストを送ることで、HTTPリクエストに対して正常にレスポンスが返ってくることが確認できます。

$ curl --dump-header - http://127.0.0.1:8080/hubot/version
HTTP/1.1 200 OK
X-Powered-By: hubot/hubot
Date: Thu, 03 Jul 2014 15:38:08 GMT
Connection: keep-alive
Transfer-Encoding: chunked

2.7.5

Hubotがデフォルトで待ち受ける8080番ポートが既に使用されている場合は、PORTオプションを環境変数で指定することで待ち受けるポート番号を変更することができます。

では、httpd.coffeeはどのようにルートを登録しているのでしょうか。httpd.coffeeから/hubot/versionへのリクエストを受け付けるコードを抜粋します。

  robot.router.get "/hubot/version", (req, res) ->
    res.end robot.version

出典:執筆時点のhttpd.coffee

robot.routerがexpressのオブジェクトです。つまり、このコードは直接expressを使う場合の次のようなコードと同等ということになります。

express = require 'express'
app = express()

app.get "/hubot/version", (req, res) ->
  res.end 'version string'

app.listen(8080)

httpd.coffeeには、他にもHubotからシェルコマンドを実行するサンプルなども含まれるため、一度ソースコードを確認することをお勧めします。

expressの具体的な使い方は、expressの公式サイトを参照してください。

GitHubのリポジトリに対するpushを通知するサンプルコード

実用的な例として、GitHubのリポジトリにpushがあった場合にHubotがチャットルームに通知するスクリプトを作ってみます。

GitHubには、Webhooksというリポジトリへのpushなどをトリガーとして予め設定したURLにHTTPリクエストを送るようにする機能があり、このWebhooksとHubotのWebサーバ機能を組み合せることで実現できます。

サンプルコードを次に示します。

module.exports = (robot) ->
  say = (message) ->
    envelope = room: "#chatroom"
    robot.send envelope, message

  ping = (payload) ->
    say "GitHubからのpingを受信しました"

  push = (payload) ->
    repository = payload.repository
    say "新しい差分がpushされました: #{repository['name']}"
    for commit in payload.commits
      say "commit: #{commit['message']}"

  robot.router.post "/github/hook", (req, res) ->
    # どのような種類のイベントかがリクエストヘッダにセットされています
    event = req.headers['x-github-event']
    # イベントの中身はリクエストボディにJSONで格納されています
    payload  = req.body

    switch event
      when "ping" then ping payload
      when "push" then push payload

    res.send 200

GitHubがイベントごとにどのようなデータを送ってくるかはリファレンスで一覧できます。サンプルでは、pushイベントをhookしてpushされたcommitメッセージを順番にチャットルームに発言するようになっています。

今回のスクリプトは、GitHubからのリクエストを待ち受けるスクリプトなので、試しに動かしてみるためにGitHubのリポジトリ設定画面からWebhooksを有効化します。

リポジトリの設定画面から⁠Webhooks & Services⁠を選択すると次のような画面が出てきます。

図5 Webhooks & Services画面
図5 Webhook

ここで、⁠Webhooks⁠⁠Add webhook⁠をクリックするとwebhookの設定画面に遷移します。

図6 Webhooksの設定例
図6 Webhooksの設定例

簡単に試すだけなら、Payload URLにリクエストの送り先を指定するだけで設定完了です。今回は、サンプルスクリプトで指定した/github/hookを送り先とします。

では、実際に対象のリポジトリにpushしてみます。

図7 GitHubのリポジトリに対するpushを通知するサンプルコードの実行例
図7 GitHubのリポジトリに対するpushを通知するサンプルコードの実行例

実際に使用する場合は、GitHub以外からのリクエストを受け付けないためにドキュメントを参照して対策を施してください。

Jenkinsのビルドの実行結果を通知するサンプルコード

もう1つの例として、Jenkinsのビルドが完了したときにHubotが発言するようにしてみます。

Jenkinsは、Notification Pluginを導入することでビルドが完了したときにその結果をHTTPリクエストで外部に送信する機能を追加できます。

GitHubのWebhooksと同様に、この機能とWebサーバ機能を組み合わせたサンプルコードを次に示します。

module.exports = (robot) ->
  say = (message) ->
    envelope = room: "#chatroom"
    robot.send envelope, message

  robot.router.post "/jenkins/hook", (req, res) ->
    headers = req.headers
    payload  = req.body

    name = payload['name']
    status = payload['build']['status']

    say "[Jenkins] #{name} : #{status}"

    res.send 200

動作を試すには、JenkinsにNotification Pluginを追加し、プロジェクトの設定画面で次のようにHTTPリクエストの送信先を指定する必要があります。

図8 Jenkinsの設定例
図8 Jenkinsの設定例

設定が反映されると、ジョブの完了時にHubotが結果を受信して次のようにチャット上に発言するようになります。

図9 Jenkinsのビルドの実行結果を通知するサンプルコードの実行例
図9 Jenkinsのビルドの実行結果を通知するサンプルコードの実行例

まとめ

本連載の最終回となる今回は、定期実行、HTTPリクエストの送信、永続化、Webサーバの使用を例に色々なユースケースに対応したスクリプトの書き方を紹介しました。

本連載では、Hubotの導入からスクリプトの書き方まで一通りの使い方を紹介しました。Hubotにはスクリプトで簡単に様々な機能を追加できるため、Node.jsの豊富なモジュールとHubotの機能を組み合わせて現場ごとに自分達に必要なスクリプトを開発してHubotを育てていくことができます。

本連載はこれにて終了しますが、皆さまの現場に様々なHubotが育っていくことを願っています。ご愛読ありがとうございました。

おすすめ記事

記事・ニュース一覧