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

第4回Hubotのスクリプトを書いてみる

今回は、Hubotのスクリプトが動く仕組みについて説明し、基本的な機能であるチャットでの受け答えを実装する方法を説明した後に、その他の機能について紹介します。

スクリプトの基本

Hubotがスクリプトを読み込み実行する仕組みを説明するために、⁠hello⁠と挨拶するとHubotが⁠hi⁠と返事する単純なスクリプトのサンプルを示します。

hello.coffee
module.exports = (robot) ->
  robot.hear /hello/, (msg) ->
    msg.reply 'hi'

このサンプルコードの一番外側を見ると、module.exportsに関数を代入しています。このmodule.exportsは、Node.jsでモジュールを作るための仕組みです。つまり、Hubotのスクリプトとは、引数を1つとる1つの関数を提供するNode.jsのモジュールということになります。

サンプルコードでは、関数の中で引数robothearメソッドを呼び出してコールバック関数を登録しています。このrobotは、Hubotの本体とも言えるRobotクラスのインスタンスです。

このように、基本的なHubotのスクリプトでは、Robotオブジェクトのメソッドを通じてコールバック関数を登録することで、⁠チャット上で誰かが特定のキーワードを発言したとき」「チャットルームに誰かが新たに参加したとき」など様々な状況ごとにHubotの振る舞いを定義していきます。

Node.jsのモジュールの詳しい仕組みは、Node.jsのドキュメントをご確認ください。

スクリプトの読み込み

では、作成したモジュール、つまりHubotのスクリプトはいつどのように読み込まれ実行されるのでしょうか。

Hubotが起動してAdapterがチャットツールと接続したとき、Hubotは./scripts./src/scriptshubot-scripts.json, external-scripts.jsonHUBOT_SCRIPTSで指定したpathからNode.jsのモジュールを探し、requireすると同時にmodule.exportsに代入された関数をRobotオブジェクトを引数にして呼び出します。

Hubot(Robotオブジェクト)が特定のスクリプトファイルを読み込むコードの例を次に抜粋します。

211  loadFile: (path, file) ->
212    ext  = Path.extname file
213    full = Path.join path, Path.basename(file, ext)
214    if require.extensions[ext]
215      try
216        require(full) @
217        @parseHelp Path.join(path, file)
218      catch error
219        @logger.error "Unable to load #{full}: #{error.stack}"
220        process.exit(1)

引用元:執筆時点のRobotクラスのloadFileメソッド

216行目で、読み込み対象のモジュールをrequireしつつexportされた関数を自分自身を引数として呼び出していることがわかります。

まとめ

スクリプトの登録から呼び出しまでの流れを図1に示します。

図1 イベントリスナの登録と実行
図1 イベントリスナの登録と実行

まとめると、まずHubotが起動してAdapterがチャットツールに接続したときに作成したスクリプトが読み込まれ実行されます。次に、実行されたスクリプトは引数で受け取ったRobotオブジェクトにコールバック関数を登録します。最後に、コールバック関数を登録するときに指定した条件を満たすと登録したコールバック関数が呼び出される、という流れになります。

ここからは、Hubotの基本的な動作をどのように定義するかについて説明します。

チャットでの発言に反応するhearとrespond

Hubotは、チャット用のbotですから、チャットで誰かが特定のキーワードを発言したときに何らかの処理を実行すると言うのが基本的な使い方だと思います。 このような場合には、Robotクラスのメソッドhearrespondを使用します。hearrespondを使ったコードのサンプルを次に示します。

module.exports = (robot) ->
  robot.hear /foo/i, (msg) ->
    msg.send "bar"

  robot.respond /hoge/i, (msg) ->
    msg.send "fuga"

respondhearの使い方について説明します。

hear

書式を以下に記します。

hear: (マッチさせたい正規表現, 正規表現にマッチしたときに呼び出されるコールバック関数)

hearメソッドは、第一引数で渡した正規表現にマッチする発言がチャット上にポストされた場合に第二引数で渡したコールバック関数を実行します。 上記のサンプルコードの場合は、/foo/iにマッチする発言があった場合にコールバック関数が呼び出され、Hubotが"bar"と発言します。

図2 hearのイメージ
図2 hearのイメージ

正規表現にマッチするならどのような発言にも反応してしまうので、たとえば、

module.exports = (robot) ->
  robot.hear /deploy/i, (msg) ->
    # デプロイ処理

と言ったコードを書くと、会話の流れで⁠deploy⁠という単語が出ただけでデプロイプロセスが走ってしまうといった問題が発生する可能性があります。 そのため、hearメソッドは、たとえば「誰かがURLがポストするとページを取得してタイトルをポストする」といった無差別にマッチしても問題無い用途で使用すると良いと思います。

respond

書式を以下に記します。

respond: (マッチさせたい正規表現, 正規表現にマッチしたときに呼び出されるコールバック関数)

respondも、正規表現にマッチする発言がチャット上にポストされるとコールバック関数が実行されるという点ではhearと同じです。 しかし、respondの場合はHUBOT_NAMEかそのエイリアスが「発言文字列の先頭」に存在するかどうかのチェックが入るという点が異なります。 たとえば、正規表現が/hoge/iだった場合、respondでは⁠hoge⁠という発言にはマッチせず、⁠hubot hoge⁠などHUBOT_NAMEもしくはHUBOT_ALIASが文頭に存在していた場合にのみマッチするということです。

図3 respondのイメージ
図3 respondのイメージ

respondは、hearとは逆に、おもにHubotに対して特定の命令を実行させたい場合に使います。ちなみに、文頭に付けるHUBOT_NAMEは通常の⁠hubot hoge⁠と言った形式の他にも@hubot hoge⁠⁠hubot: hoge⁠といった異なった形式も使用できます。

注意点として、respondメソッドは、渡された正規表現を分解して⁠文頭にHUBOT_NAMEが存在するか⁠という条件を元々の正規表現の先頭に追加した形で正規表現を再構築するため、^を使用しても動作しないといった問題に遭遇することがあります。

チャットに発言を投稿するsendとreply

hearrespondと同様にHubotにチャットで発言させる方法にもsendreplyの2種類があります。

sendreplyを使ったコードのサンプルを次に示します。

module.exports = (robot) ->
  robot.hear /foo/i, (msg) ->
    msg.send "bar"

  robot.respond /hoge/i, (msg) ->
    msg.reply "fuga"

これまで書いてきたコールバック関数は、すべてmsgという仮引数を持ちますが、この引数にはResponseクラスのインスタンスが渡されます。Hubotのスクリプトでは、このResponseオブジェクトを通じてイベントが発生したときの状況を取得したり、チャットツールに対して様々なアクションを取ることができます。

チャットに発言するには、Responseクラスのメソッドsendもしくはreplyを使用します。

send

書式を以下に記します。

send: (発言内容...)

sendメソッドにHubotに発言させたい文字列を渡すことでHubotに発言させることができます。 引数は可変長となっており、複数の文字列を渡すことで複数の発言を同時に送ることもできます。

図4 sendのイメージ
図4 sendのイメージ

reply

書式を以下に記します。

reply: (発言内容...)

replyメソッドを呼び出すと、sendメソッドと同様にHubotが発言しますが、sendメソッドとは「イベントが発生した元となった発言者に対しての返答」という形で発言する点が異なります。 この「返答」の定義はチャットツールやAdapterによって異なります。たとえばhubot-ircでは元の発言者名をHubotの発言の文頭に付与します。

図5 replyのイメージ
図5 replyのイメージ

発言内容のキャプチャ

ここまでの内容で、チャット上の特定のキーワードに反応して処理を実行したり、それに合わせてHubotに発言させることができるようになりましたが、キーワードごとに固定の処理しかできないのでは不便です。Hubotは、hearrespondのどちらを使用した場合でも、正規表現にマッチした発言の内容を取得して使用することができます。

発言内容をキャプチャするコードのサンプルを次に示します。

module.exports = (robot) ->
  robot.respond /deploy (.+)/i, (msg) ->
    msg.send "deploy: #{msg.match[1]}"

respondhearの第一引数に渡す正規表現がチャット上の発言にマッチした場合、コールバック関数の引数に渡されるResponseオブジェクトのmatchプロパティにString.matchメソッドの戻り値が格納されます。

たとえば、上記のサンプルコードに対して⁠hubot deploy web01⁠と発言した時のmsg.matchの中身を次に示します。

[
  'hubot deploy web01',
  'web01',
  index: 0,
  input: 'hubot deploy web01'
]

普通にString.matchメソッドを使用した場合と同じく、0番目の要素にマッチした文字列全体が、1番目以降にキャプチャした文字列が格納されます。キャプチャした文字列を使用することで、単純にキーワードに反応するだけでなく、発言内容に応じた様々な処理を実装できます。

その他

Hubotのスクリプトでは、ここまでで紹介した内容の他にも様々な機能を実装できます。すべてを紹介することはできないので、ここではいくつかの項目に絞って説明します。

チャットルームの入退室に反応する

Adapterが対応している場合、チャットルームへ誰かが入退室した時にHubotに何らかのアクションを起こさせることができます。

module.exports = (robot) ->
  robot.enter (msg) ->
    msg.send "ようこそ #{msg.message.user.name}-san"

  robot.leave (msg) ->
    msg.send "さよなら #{msg.message.user.name}-san"
図6 enter/leaveの動作イメージ
図6 enter/leaveの動作イメージ

トピックの変更に反応する

Adapterが対応している場合、チャットルームのトピックが変更された場合に何らかのアクションを起こさせることができます。

module.exports = (robot) ->
  robot.topic (msg) ->
    msg.reply "トピックを変更しないでください #{msg.message.user.name}-san"

なお、hubot-ircは本稿執筆時点では対応していません。

環境変数からの設定の読み込み

前回Hubotのおもな設定は環境変数を通じて行うと説明しました。 自作のスクリプトでも、通常のNode.jsで行うのと同様にprocess.envを使用することで環境変数に設定された値を読み取ることができます。

環境変数を読み取る例を次に示します。

optional_value = process.env.HUBOT_DEPLOY_KEY

module.exports = (robot) ->
  robot.respond /deploy (.+)/i, (msg)  ->
    unless optional_value?
      msg.reply "HUBOT_DEPLOY_KEYを設定して下さい"
      return

    # デプロイプロセス

    msg.reply "デプロイが完了しました"

エラーハンドリング

Hubotの実行中にcatchされなかった例外が発生した場合、HubotはNode.jsのuncaughtExceptionイベントを受け取って独自のerrorイベントを発生させます。Robotクラスのerrorメソッドを使用することで、errorイベントが発生した時に独自の処理を実行することができます。

errorイベントを処理する例を次に示します。

module.exports = (robot) ->
  robot.error (err, msg) ->
    console.log err

また、Robotクラスのemmitメソッドを使用することで自分のスクリプト内で意図的にerrorイベントを発生させることもできます。

module.exports = (robot) ->
  robot.error (err, msg) ->
    console.log err

  robot.hear /foo/, (msg) ->
    robot.emit 'error', new Error('error'), msg

Responseオブジェクトについて

ここまでの説明でReponseオブジェクトの使い方をいくらか説明してきましたが、Hubotを使ううえでRobotオブジェクトと並んで使用頻度の高いオブジェクトですので、もう少し詳しい内容を説明します。

メソッド

Responseクラスのメソッド一覧を次に示します。

メソッド名用途
sendチャットルームに通常の方法でメッセージを送る
emote感情表現をチャットルームに送る
reply発言者に対する返信としてメッセージを送る
topicチャットルームのトピックを変更
play音を鳴らす(ほぼCampifire専用)
lockedログに残らないように発言する(ほぼCampfire専用)
randomリストを受け取るとそのリストの中からランダムに1つ選択
finish今回のメッセージに対してリスナの実行を終了
httpscoped-http-clientを使用してHTTPリクエストを作成

これらの多くはAdapterのメソッドを呼び出しているため、具体的な動作はAdapterの実装に左右される点には注意が必要です。

playなどのあまり見かけないような機能を持っているのは、おそらくHubotが元々Campfire用のツールとして作られていたことによるようです(Campfireには音を鳴らす機能があります⁠⁠。 他にも、lockedメソッドもCampfire用のAdapterには実装がありますが、本稿執筆時点では元となるAdapterクラスにはメソッド定義が無かったりします。

お詫び

前回トピックの変更方法についてAdapterを直接操作する方法で紹介していました。

しかし、実際にはResponseオブジェクトのtopicメソッドを使用することでAdapterが対応しているならばAdapter間の互換性を維持した状態でトピックの変更操作を実装できます。 誤った情報を記載してしまい申し訳ありませんでした。

尚、現在は修正済みです。

プロパティ

Responseオブジェクトのプロパティのうち普段使うのは大きく分けてmessagematchの2種類です。

このうち、matchrespondhearなど正規表現でマッチさせる種類のイベントの場合はString.matchの戻り値が入っており、それ以外の場合には基本的にtrueが入っています。 この仕組みは、ListenerRobotのコードを見るとわかりやすいと思います。

messageプロパティには、イベントの発生源から送られてくるメッセージが入っています。つまり、イベントの種類やAdapterの実装によって中身が変わる場合があります。Responseオブジェクトからmessageプロパティの内容を抜粋した例を次に示します。

   message: {
      user: { id:'user', name:'user', room:'#chatroom' },
      text: 'hubot ping',
      id: undefined,
      done: false,
      room: '#chatroom'
   },

上記の例は、hubot-irc⁠user⁠という名前の参加者が⁠#chatroom⁠という名前のチャンネルで⁠hubot ping⁠と発言した場合のメッセージです。 とくに、発言者の情報はスクリプトから使用する機会が多いのではないでしょうか。

自分が使用しているAdapterでどのようなメッセージが送られてきているかを確認するには、Adapterのドキュメントやソースコードを読むか、hearrespondなどに渡すコールバック関数で引数のmsgconsole.logで出力してみるといった方法が簡単です。

まとめ

今回は、Hubotのスクリプトの基本的な書き方を説明しました。

普段はhearrespondsendreplyを使うことが多いと思いますが、Hubotにはあまり使われていない機能を含め色々な機能があるのでぜひ使いこなしてみてください。

次回は、サンプルコードやhubot-scriptsのコードなどを例に様々な応用方法を紹介します。

参考文献

おすすめ記事

記事・ニュース一覧