BotkitでSlack Botを作りつつ自己アップデートできるようにした

Slack皆さん使ってますか?

Slackはただチャットが出来るだけではなく豊富なAPIによってもたらされる便利Botたちの存在が非常に大きいです.

Botを開発する際には,Githubが開発しているHubotが有名ですが,ここ最近ではSlack botに特化した(別に特化しているわけでもなさそう)BotkitというBotフレームワークが出ました.

github.com

Botの設置自体はリポジトリのREADME.mdを見ればわかるので,見てください. で,自己アップデートというのは何かって言うと,Botのコードを書き換えてpushした後に設置しているサーバにログインして,pullしてrestartするっていう作業がめっちゃだるかったので,自分で自分を更新して再起動させる仕組みを作りました.

基本的なコンセプトは HubotでHubotの更新をforeverを利用してHubotにさせてみる - MANA-DOT ここから得ました.ありがとうございます.

foreverでの起動

まず,参考ページの通りforeverでの起動をします. 元リポジトリの起動方法だと,tokenを環境変数で与えるようになっていますが,foreverでの環境変数の与え方がわからなかったので,Botのソース内に直書きするかファイルを読む仕組みにしましょう.

forever start bot.js

これで起動はできます.

参考ページにもある通り,foreverは死んでもうまいこと生き返らせてくれる機能を持っているので,アップデート処理が終わったら自殺するというコードをかけば良いでしょう.

アップデート処理

まずはコードを出します.

var child_process = require('child_process');

function updateSelf(bot, message){
  child_process.exec('git reset --hard origin/master', function(error, stdout, stderr){
    bot.reply(message, 'Botが更新されました!');
    bot.reply(message, 'Botを再起動します');
    setTimeout(function(){
      process.exit();
    }, 2000);
  });
}

controller.hears(['update'], 'direct_mention', function(bot, message) {
  bot.reply(message, 'Botのアップデートを開始します');

  child_process.exec('git fetch', function(error, stdout, stderr){
    child_process.exec('git log master..origin/master', function(error, stdout, stderr){
      if(stdout == ""){
        bot.reply(message, 'Botは最新です');
      }else{
        bot.startConversation(message, function(error, convo){
          bot.reply(message, '更新内容は以下のとおりです');
          bot.reply(message, '```\n' + stdout + '\n```');
          convo.ask('アップデートを行いますか?(y/n)', [
            {
              pattern: bot.utterances.yes,
              callback: function(response, convo){
                updateSelf(bot, message);
                convo.next();
              }
            },
            {
              pattern: bot.utterances.no,
              callback: function(response, convo){
                bot.reply(message, 'アップデートを中止します');
                convo.next();
              }
            },
            {
              pattern: 'じゃあそれで',
              callback: function(response, convo){
                updateSelf(bot, message);
                convo.next();
              }
            },
            {
              default: true,
                callback: function(response, convo){
                  convo.repeat();
                  convo.next();
                }
            }
          ]);
        });
      }
    });
  });
});

まずgitを操作したりするために,コマンド実行可能な child_process を利用します. update コマンドが発行された時にまず,git fetchを行い,アップデートの確認を行います. アップデートがあるのであれば,なんらかの標準出力がなされるので,それでアップデートがあるかどうかを判定します.

アップデートがある場合は,変更点を git log で出力するようにしました. また,BotkitのConversation機能を用いて,アップデートを行うかどうかの確認を行う仕組みを導入しました.

アップデートが許可された場合には, git reset --hard origin/master とすることで,origin/master への追従を行い,最新版へのコード更新が行われます. その後, process.exit() を発行し,自殺した後foreverによって再び生かされることになります.

process.exit() を発行するときの setTimeout が無いと,bot.reply() が間に合わず何も出力されないケースがあるので,適当に時間とって setTimeout した方がいいです.

実行時の様子

f:id:programmerMOT:20160226125551p:plain

終わり

これでBot開発が捗る.

記事を自分で読み返して, git fetch しておいて,アップデートを了承しなかった場合,もう一度updateした時には,fetchが働かず永久にupdateできないというバグを見つけてしまった. アップデートがあるかどうかの判定を git log master..origin/master でやれば良さそう.