プログラミング初心者のアマグラマーがhubot相手に奮闘中

はじめに

こちらはアドベントカレンダー9日目の記事です。

何か書かねばならぬ流れになり、なかなかコーディングのプロになれないアマチュア以下レベルの私が hubot奮闘記 について書こうと思います。 当方Photosynth入社2ヶ月、ぺーぺーQAです。開発エンジニアではないので技術的な部分についてもしかしたら誤りがあるかもしれません。以下の点を予めご了承いただきご一読していただければと。

  • 本記事は「これをやったらできた」というログです。技術的な根拠を保証するものではありません
  • hubotを動かすための奮闘記です
  • hubotのインストール〜セットアップについては触れません
  • 実装環境はMac(Mojave)です

背景とここまでの流れ

この記事を記載するまでのざっくりとした背景と流れ(当方視点)は以下の通り

  1. テスト自動化の話があがっていた
  2. 諸々検討の末(はしょります)、「自動化」の切り口をテスト自動化に拘らず業務全般(プロセス)に置いた
  3. botを使って誰でも簡単に何かできるようにしたい」という意向があった
  4. 当社チャットツールはslackを使っている
  5. 前職で slack & hubot 使ってた(使ってただけで裏側はいじったことがなかった)
  6. 前職の経験からいろいろと融通が効きそうなhubotを提案してみた
  7. 提案が通った
  8. 目的(解決する課題)を定義
  9. 実装開始

目的(解決すべき業務課題) と 手段(hubotをどう使うか) の設定

目的

↑の2で記載した 何か をどう置くか。その今回は目的部分を「誰でも触れる簡易デバッグツール」にしました。 データ参照という点では将来的にCSさんや営業さんが出先でもslackで一言発言すればユーザー様の利用情報など参照できるようにしたいと考えております(現状はその前段階で奮闘中)。将来的に。 そのための社内APIもhubotとともに実装したりしていますが今回それは別の話。hubot自体に焦点を当てて記載します。

手段

hubotで実現したい要件(ざっくり)は以下の通り

  • slack上で任意のワードに反応する
  • ワードに応じたAPIを叩きに行く
  • API叩いて返ってきた内容をbotがslackで発言してくれる

とっても初歩的なhubotの活用術ですね。 それでも当方のようなアマグラマーにとってはなかなかのハードル。あーでもないこーでもないと捏ねくり回しております。

f:id:photosynth-inc:20191209184352p:plain
hubot_image

hubothubotっていうけどそもそもhubotって何?

slack(チャットツール)で使えるボットです(ざっくり)。 もう少し詳しく言うと GitHub社が開発しMITライセンスで公開しているNode.jsでbotを作り動かすためのフレームワークです(引用参照)。

要するに チャットツールをちょっと便利にしてくれるツール という認識。

立ちはだかる壁 〜言語問題編〜

さて前置きが長くなりました。ここからが本題です。 早速実装を、、と思ったらこのhubot、coffee scriptなんですよね… coffee script を0から覚えていくのはいかがなものか、、coffeeとか生理的に受け付けない、、、。 なんとかcoffee回避できないか、色々考えました。

考えてみた その1

coffeeはあくまでhubotとコミュニケーションするためのハブとして使い、別言語の実装ファイルを呼び出して使う

HubotSlack = require 'hubot-slack'
child_process = require('child_process')
module.exports = (robot) ->
    robot.hear /^example (.*)$/i, (res) ->
        child_process.exec "bash shell/test.sh", ()->

こうすればcoffeeにふれるのは最低限で済む。万事解決......と思いきや。

結果
  • そもそもshellで書くってハードル高すぎるだろう…属人化招く一因にもなりうるんじゃね?
  • エンジニアさんにも「やめとけ」と首を横に振られた
  • じゃrubyで…
  • ハブ扱いにすると色々と処理がめんどい

などなど。この現実逃避は失敗しました。

考えてみた その2

流行り(?)のgasと連携させれば良いのでは…?

結果

先のslackのアップデートによりwebhooksの扱いがうんにゃらということで心折られました。(こちらの記事の冒頭部分参照) [slack × hubot × gas]関連はこの記事以前に書かれているものが多く、解決しうる記事が見つからず。 当方の力量では皆様のお書きになられ遊ばれた御ドキュメントのお力添えをなくして実現不可と存じ上げ候。

考えてみた その3

この(coofee嫌いという)問題に遂に決着が付きました。 coffeeのcommandでこんなものが。

$ coffee --compile script/example.coffee

hubotインストール時に example.coffee なるファイルが存在します。hubotはこうやって使えよっていうサンプル用実装ファイルです。 そのexample.coffeeに対して上記のコマンドを実行してみたところ、元ファイルとは別に example.js なるファイルが生成された(歓喜) しかも生成されたファイルのコード、結構わかりやすい(歓喜

module.exports = (robot) ->
  robot.respond /テスト/i, (msg) ->
    msg.send "テスト"

これが

// Generated by CoffeeScript 1.6.3
(function() {
  module.exports = function(robot) {
    return robot.respond(/テスト/i, function(msg) {
      return msg.send("テスト");
    });
  };

}).call(this);

こう。

ここでは example.coffee を例にしましたが、実際ちょろっと作ってたコードも分かりやすくコンパイルしてくれました。 もちろん .js ファイルでhubotさん問題なく動いてくれます。

こうしてjsで書く方針を固め、実装を開始するのでした。

立ちはだかる壁 〜デバッグ編〜

実装をすすめるにつれ、実挙動を確認しながら実装する = 都度hubotを立ち上げてコード検証を行う のがダr...もといダルくなってきました。

jsコードのデバッグ

jsコードをターミナル上でなんとか実行できないものか。調べてみたらMacには jsc なるものがデフォでインストールされているとか。 jsファイルを jsc hogehoge.js と打つと実行してくれる模様。

使ってみよう

$ jsc --help
-bash: jsc: command not found

あかんやん。 パスが通っていない(シンボリックリンクを作る必要がある)ため、Macさんの素の状態では使えないようです。 以下を実行します

$ ln -s /System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc /usr/local/bin

そしてもう一度

$ jsc --help
Usage: jsc [options] [files] [-- arguments]
  -d         Dumps bytecode (debug builds only)
  -e         Evaluate argument as script code
  -f         Specifies a source file (deprecated)
  -h|--help  Prints this help message
  -i         Enables interactive mode (default if no files are specified)
  -m         Execute as a module
  -s         Installs signal handlers that exit on a crash (Unix platforms only)
  -p <file>  Outputs profiling data to a file
  -x         Output exit code before terminating

<以下略>

出来た(歓喜)。 jsの挙動に詳しくないのでtest.js なる適当なファイルを作って jsc test.js を実行し、変数の中身や振る舞いを確認しながら実装を進めています。こうすることで大分楽になりました。

※ jscでは consol.log() はそもそもの実装がないため使えません。print()を使いましょう。

正規表現デバッグ

hubotをslack上で呼び出すにあたって、自然言語だとhubotさんが困ってしまうかもしれない(割愛)と思い、 ターミナルでcommandを実行するような挙動にしたいと考えつつ実装をすすめています。 例えばこんな感じ

index device 1000000 # <引数1:command> <引数2:target> <引数3:id>
# => idが1000000のデバイスの情報を参照して返す

ここで必要な要件は

  • 有効な引数以外の引数が入力されたときは弾く
  • コマンドに対して必要な引数の数が不足・超過しているときは弾く

など。

return robot.hear(/(.*) (.*) (.*)$/i, function(msg) {略}

(.*) (.*) (.*) ←ここをしっかり書かねば、、でもいちいち動かして確認はツライ。 そしてたどり着きました.

https://rubular.com/

便利です(歓喜)。 rubyと記載がありますが「正規表現だし関係ねぇ」と思って使ってます。今の所jsの挙動と差異はありません(詳しいことはわかりませんすみません)。

これを基に上記の例でいうと (.*) (.*) (.*)^index \w* \d{6} 雑ですがこんな感じかな、、と。皆様もぜひ触ってみてください。

※slackからの引数の活用として以下のように書けば必要以上の正規表現は書かずに済みそうです。 結論、現在はswitch文でカバーの方針に切り替えています。正規表現の扱いに苦労しましたというお話でした(

module.exports = function(robot) {
    return robot.hear(/^device (.*)$/i, function(msg) {
      var arg = msg.match[0].split(/\s/);
      // => ["device","引数2","引数3","引数4",....] 
//<以下略>

立ちはだかる壁 〜API編〜

APIを作るのがhogefugapiyoというのも壁ではあるのですが本記事に関係ないので割愛します。 問題は「APIをどうやって叩けばええねん」ということでした。

https://hubot.github.com/docs/scripting/

こちら公式ドキュメントなのですが、 ここの Making HTTP calls を読んでもイマイチわかりません。

やりたいこと
  • <引数:id>をAPIに渡してDBを参照したい
  • <引数:id>をAPIに渡してDB情報の一部をupdateしたい(書き換えたい)

英語をnative言語とするエンジニアさんに助けを請い、以下のようにしないといけないことが分かりました。

結果
参照する場合
module.exports = function(robot) {
    return robot.hear(/^device index <id>$/i, function(msg) {
      var arg = msg.match[0].split(/\s/);
  
      robot.http("http://localhost:8000/API/device/index")
        .header('Content-Type', 'application/json')
        .query({id: arg[2])
        .get()(function(err, res, body) {
          // APIから返ってきたjsonを連想配列に変換
          var json = JSON.parse(body);

//略

APIに渡すデータを get記述の前に.query()に記述する必要があるようです。 ※このクエリの中身についてはXMLでもいいみたい?ですが当方このパターンしか試していません。あしからず。

DBを書き換える場合
module.exports = function(robot) {
    return robot.hear(/^device index <id>$/i, function(msg) {
      var arg = msg.match[0].split(/\s/);
      var data = JSON.stringify({id: arg[2]}); //json型に整形
      robot.http("http://localhost:8000/API/device/update")
        .header('Content-Type', 'application/json')
        .put(data)(function(err, res, body) {
          // APIから返ってきたjsonを連想配列に整形
          var json = JSON.parse(body);

//略

APIに渡すデータ .put() の引数にjson型で入れる。

まとめ

HTTPリクエストの際に使うコマンド(getとかputとか)に応じてリクエストヘッダの記述は異なります(詳しくは知りません…)。 hubot(js??)での叩き方はもちろんですが、HTTP自体の知識も最低限つけなければあかんなぁと切に感じました。

全体まとめ

こんな感じで絶賛奮闘中です。 それってQAがやることなん?と疑問に思う方もいるかも知れませんが、QAの仕事は品質を作り込むことです。 テストだけが業務ではなく、開発プロセスやサービスリリース後のプロセスにおいても作り込める品質があります。

開発中であれば「そもそもバグが仕込まれないプロセスを組むこと」もその一つ。まずマイナスを作り込まないようにすること。 テストはマイナスをゼロにする作業です。 リリース後であれば挙げ始めると切りがありません。

ざっくりとまとめると、今回の取り組みはhubotを使って今まで手間だったことを機械的に解決しようというものでした。 いろんなプロセスで「それって人の手でやることじゃないよね?」とか「開発にボール投げずに参照とか解決したいよね」とかって意外と多いんですよね。 将来どこかのプロセスで、今の奮闘をベースにその辺り解決できるちょっと便利な機能として社内で役立つといいなと思います。 しっかりとコードを書けるQAになるべく精進して行きたいと思います(という意思表明)