SlackとスプレッドシートとHubotで非エンジニア向けの運用ツールとして使ってみる

非エンジニアのオペレーターの人のために管理画面を作るのが面倒で、
Slack + Googleスプレッドシートでデータを更新作業をしてくれるHubotを作ったら結構良かった

例えば、こんな感じのランキングモジュールがありました。

<img width="316" alt="スクリーンショット 2016-06-23 19.26.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/7132/1432e287-978f-cae0-ec10-bf86183e4eb5.png">

システムや運用の都合で自動化せずに、非エンジニアの人が手動で毎週更新するという仕様です。


## 更新の流れ

### １．データをGoogleスプレッドシートに記入する。
<img width="351" alt="スクリーンショット 2016-06-24 0.20.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/7132/af2d9859-3ee5-d30d-981f-ae491a4e76a7.png">
> 表示させたいコンテンツに対応するIDを記入してもらう

### ２．SlackからHubotで更新させる。
<img width="605" alt="スクリーンショット 2016-06-23 19.31.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/7132/ad617cd4-ee91-9f9c-4971-b4801c4c10a9.png">
> Hubotへメンションを送るだけ

### ３．おわり
> オペレーターの作業はこれだけ
> 普段使っているツールなので馴染みやすいです



## 仕組み

SlackのメンションをHubotで受け付け、GASでスプレッドシートの内容からJSONを生成、そのJSONをStaticサーバーにアップロードし、そのデータをサービス側のサーバーから取得するという流れになります。

文字で説明するとわけわからんので下図
![Artboard 1.png](https://qiita-image-store.s3.amazonaws.com/0/7132/a4fd1989-88f3-4191-bebe-6524dbb8ff57.png)


### １．Slackのメンションを受け取る

hubot-slackのライブラリを読み込む

```json:package.json
{
  "dependencies": {
    "hubot-slack": "^3.4.2",
    "hubot-slack-attachment": "^1.0.0",
  }
}
```

Slacｋのトークンを設定

```bash
$ HUBOT_SLACK_TOKEN="xxxxxxxxxxx"
```

Slackからメンションを受け取るにはこれだけ

```coffeescript:update.coffee
module.exports = (robot) ->
  robot.respond /ランキングデータ更新/i, (msg) ->
    console.log "更新依頼受付"
```

これで```ランキングデータ更新```の文字列が含まれるときに実行されるようになります。


### ２．GASでスプレッドシートのデータを取得するAPIを作る

こんなかんじの構成のシートがあったとします

|monthly|weekly|
|---|---|
|11111|66666|
|22222|77777|
|33333|88888|
|44444|99999|
|55555|12345|

取得生成コード

```js:getRanking.gas
function doGet(e) {
  var url = "https://docs.google.com/spreadsheets/d/xxxxxx/edit#gid=0"; // 取得ファイルのURL
  var sheetName = "sheet1"; // 取得するシート名
  
  var book = SpreadsheetApp.openByUrl(url);
  var sheet = book.getSheetByName(sheetName);
  
  var json = getData(sheet);
  var res = ContentService.createTextOutput(JSON.stringify(json));

  return res.setMimeType(ContentService.MimeType.JSON);
}

function getData(sheet) {
  var monthly = [];
  var weekly = [];

  var lastRow = sheet.getLastRow();
  for(var rowIndex=2; rowIndex<=lastRow; rowIndex++) {
    var month = sheet.getRange(rowIndex, 1);
    var week  = sheet.getRange(rowIndex, 2);
    
    if (month.getValue()) {
      monthly.push(month.getValue());
    }
    if (week.getValue()) {
      weekly.push(week.getValue());
    }
  }

  return {"monthy":monthly, "weekly":weekly};
}
```

#### ```公開``` → ```ウェブアプリケーションとして導入``` でAPI化
<img width="563" alt="スクリーンショット 2016-06-24 1.11.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/7132/9120f85b-580d-cd3c-a34f-836c8ca05a03.png">
Hubotからのアクセスが面倒なのでとりあえず、```全員（匿名ユーザー含む）```にします。

できあがったAPIを叩くと、こんなJSONが返却されるはず

```json
{
  "monthy": [
    11111,
    22222,
    33333,
    44444,
    55555
  ],
  "weekly": [
    66666,
    77777,
    88888,
    99999,
    12345
  ]
}
```

### ３．HubotからGAS APIを取得してファイルを保存

npmの```request ```、```fs```を読み込む

```coffeescript:update.coffee
request = require 'request'
fs = require('fs')
```

取得保存のコード

```coffeescript:update.coffee
sheetApi = "https://script.google.com/macros/s/xxxxx-/exec" # GASで生成したAPI
tmpFile = "/tmp/ranking_" + new Date().getTime() + ".json" # 仮保存用ファイルのパス

request {
  url: sheetApi
}, (err, response, body) ->
  if err
    console.error err
    return
  
  # json形式が正しいかチェック
  JSON.parse(body)

  # ファイルとして保存
  fs.writeFileSync(tmpFile, body)
```

### 4．HubotからSCPでStaticサーバーへアップロード

NginxでStaticサーバーを用意し、もしGoogle側で何かあったとしても吸収できる構造にします。

npmの```scp2```を読み込む

```coffeescript:update.coffee
client = require("scp2")
```

アップロードして、成功したらSlackへ成功通知

```coffeescript:update.coffee
client.scp(
  tmpFile,
  {
    host: "127.0.0.1",
    username: "username",
    password: "password",
    path: "/var/www/nginx/public/ranking.json"
  },
  (err) ->
    if err
      console.error err
      return
    
    data = {
      text: "ランキングデータを更新いたしました！"
      channel: "test" #通知するチャンネル名
    }
    robot.emit "slack.attachment", data
)
```

> この部分はGit+capistranoとかにすれば、もっと良い感じになるかなと思います。

### 5．サービス側からStaticサーバーのranking.jsonを取得
普段と同じように、APIを取得する感じで書いて、クライアントに表示する


## おわり
Slackをインターフェイスにしたら、使い方を教えやすいし、わかりやすい、
オペレーター業務がSlack上に流れるので更新されたタイミングが残るのも良かったです。
ただフォーマットがズレるとエラーになったりするため（上記の例だとIDの数値ではなく、文字列が入ってしまったなど）、エラーチェックなどのオペレーションミスを防ぐ仕組みはなにかしら必要です。

手順は多いですが、ローテクの寄せ集めなので作るのは結構簡単です。
どっちもHubotもGASもECMAScript仕様なので覚えなきゃいけないことは少なく
なにより、この機能のために管理画面のUIや**DBを用意しなくても良い**のが楽。