チャットで共有した情報、ストックされず困ることってありませんか？完全無料で解決してみる

この記事は[エイチームブライズ/エイチームコネクト/エイチーム引越し侍 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/hikkoshi)8日目の記事です。

#TL;DR

* 社内で「拡散したい」かつ「後から振り返りたい」という、フローとストック両者の長所を活かした情報共有って難しいよね
* Chatworkでハッシュタグ付き投稿すると、自動的にストックする仕組みを完全無料で実装してみた

前置きがだいぶ長いので、技術的な話を読みたい方は中盤までスルスルッと読み飛ばしてください。

イメージは以下の通りです。

このようにChatworkで発言すると
![sc20.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/56770bdc-b6e9-5394-e7e8-b3003cb05375.png)

こんな感じに保存される仕組み
![sc19.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/574cd35b-5584-a0f3-fef0-2bf424661565.png)

#開発に至った経緯

##自社サービス開発での日常

自社でWebサービスの開発・運営をしていると、自社内の動きだけでなく

* **「競合サービスがこんな戦略を取り始めた」**
* **「同じ業界で、こんなサービスが立ち上がったらしい」**
 
といったニュース、いわゆる「業界の動き」にも気を配る必要があります。

そしてこれらニュースをチャットツール(※)で社内共有し、意見交換することで自社サービス戦略の判断材料にすることがあります。
※弊社ではChatworkを導入しているため、以後Chatworkと表記

##こんな経験ありませんか？

それからXヶ月後・・・

* 自社Webサイトの数字がここ数ヶ月下降傾向だ
* 社内で大きな新機能リリースはしていない。つまり内的要因ではなさそうだ
* そういえば、下がり始めた頃に他社で変わった動きはなかったか？外的要因を探りたい
 
という話題が挙がります。

ですが当時共有した外的要因のニュースはチャットなので流れてしまい、Chatwork上で検索してもノイズが多く情報にたどり着けないことや、そもそも共有されていないことがよく発生します。

それだと困るので、「業界の動き」をChatworkだけじゃなく

* 社内wikiに書き溜めよう
* スプレッドシートに書き溜めよう

と話題に挙がり運用が開始されるものの、運用が定着せずストックされない状態になってしまいます。
もっと気軽に発信＆溜めていくことができれば、やり取りも活発になって分析の精度も上がるのに！

##情報の種類について考える

なぜ上記のようなトラブルが起こるのか考える前に、そもそも情報の種類を意識する必要があります。

共有される情報には

* フロー：メール、SNS、チャット、ニュース等の流れる情報。情報の寿命は短いが、拡散性や速報性は高い。
* ストック：wiki等の情報共有システムに溜めていき、必要な時に調べる用途で共有される情報。情報の拡散性や速報性は低いが、寿命は長い。

の2種類があります。

##なぜこのような問題が起きるのか

* 「今日カレー食べたい」→雑談なのでストックの必要がなく「100％フロー情報」
* 「社内の福利厚生紹介」→速報性が低いので「100％ストック情報」

というように、「フロー」「ストック」が明確な情報に関しては運用がうまく回っていることが多いです。

ですが今回のように「フロー」「ストック」両者で情報共有をしようと思うと、

> * 社内wikiに書き溜めよう
> * スプレッドシートに書き溜めよう
>
> と話題に挙がり運用が開始されるものの、運用が定着せずストックされない状態になってしまいます。

上記の通り「ストック」が定着しないことが多く発生します。

考えられる要因は

* フロー・ストック両者のメリットを享受する為に、2つのツールで投稿が必要。インプット量が2倍（＝投稿が面倒くさい）
* ストックする場所の管理が煩雑で、「あれ？どのファイル／どのwikiページに書くんだっけ？」といったことが頻発し、書かれなくなっていく（＝ストック場所の管理が面倒くさい）

あたりとなります。

![before.png](https://qiita-image-store.s3.amazonaws.com/0/199678/5064fc49-cbed-b910-b4a9-57b69c9b933c.png)


##解決法

なんとなく「インプット方法の質」に課題がありそうなので、ユーザーの1アクションで2アクション発生させる仕組みを考えます。

* Chatwork（フロー）に投稿すると、社内システム（ストック）にも同時投稿される
* 社内システム（ストック）に投稿すると、Chatwork（フロー）にも同時投稿される

の2通りの方法を考えてみました。

今までのケースでいくと「ストックが定着せず、フローオンリーになってしまう」ことが多かったので、今回の手法は前者の

> * Chatwork（フロー）に投稿すると、社内システム（ストック）にも同時投稿される

こちらを採用します。

#採用する技術

今回は

* [Chatwork Webhook](http://developer.chatwork.com/ja/webhook.html)
* [Google Apps Script](https://developers.google.com/apps-script/)
* [Google Fusion Tables](https://developers.google.com/fusiontables/)

を使っていきます。

![after.png](https://qiita-image-store.s3.amazonaws.com/0/199678/0ccceb54-5f0f-b1f8-f2d5-78c8cb1a7fe7.png)


選定理由は主に2つで、

* 完全無料で使えること
* 個人的にAppsScript大好きで、それを使いたかったこと

になります。

結論としては今回の構成はいろいろ面倒な点が多いので、「完全無料」をどうしても譲れない場合を除き、Chatwork Webhook + AWS Lambda + API Gateway + DynamoDB等で構成した方が柔軟性が高いです。

とはいえ社内で費用が発生すると社内承認やら費用対効果やら考えることが多いので、スモールスタートする意味でも今回の構成でスタートしています。

##Chatwork Webhookとは

Chatworkで発言した際にWeb上のAPIを叩きにいくような処理が実装できる仕組みです。
ビジネス向けChatworkであれば費用はかかりますが、API自体に追加費用等はかかりません。ただしAPI使用時はシステム管理者の許可が必要だったりするので注意。

去年のアドベントカレンダーで記事を書いているのでよろしければどうぞ。

[【Google Apps Script】その19　Chatwork Webhookを使い、スプレッドシートに発言を溜める](https://qiita.com/rf_p/items/897d93f7beca83cd5902)

##Google Apps Scriptとは

Googleが提供している、スプレッドシートやGmail等のGoogle Appsを操作できるJavaScriptベースの言語です。
これも同じく、去年アドベントカレンダーに書いているのでそちらをご確認ください。
[【Google Apps Script】その1　Hello, world!](https://qiita.com/rf_p/items/0535ec495df8f9d2b894)

2018/12/12追記
https://9to5google.com/2018/12/11/google-fusion-tables-shutting-down/
Fusion Tablesサービス終了のお知らせ。
この記事投稿から数日しか経ってないよ…！

##Google Fusion Tablesとは

スプレッドシートやGmailといったGoogle Appsの一つで、いわばGoogle版Access。
簡易的なデータベースサービスを完全無料で使うことができます。

ただしあくまでも簡易版なので、MySQLやPostgreSQLのようにプロダクトで運用できるレベルではなく、「簡易的な社内システムや個人利用で使えるかも」レベルのことしかできないです。

こんな感じ
![fusiontables.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/3aa2500c-2b13-1e8b-0bec-82f19456f42a.png)

Fusion Tablesで調べると古い記事しか見つからず、マイナーなサービスであることが分かります。
数年前書かれた記事によると「複数件UPDATEする際、PrimaryKeyに相当するROWIDというカラム指定じゃないとできない」等の情報が出てきますが、知らない間にだいぶ解消されたらしくそこそこ使えるレベルにはなっています。

…と思いましたが、「OR句が使えない」のは割と致命的。

最悪の場合使いづらくてもcsvエクスポート等もできるので、他のDBに移行するのもそこまで苦ではないはず。

#開発してみる

##Fusion Tablesの有効化

まず、Fusion Tablesは数年前からずっとベータ版なので、デフォルトのGoogle Appsでは使えません。

GoogleDriveの新規ボタンから「その他」を選び、「アプリを追加」を選びましょう。

![sc1.png](https://qiita-image-store.s3.amazonaws.com/0/199678/8d1a8051-8afc-d55e-4db0-023990a604c5.png)

検索ボックスに「fusion tables」と入力し、Fusion Tables（試験運用）を接続。
![sc2.png](https://qiita-image-store.s3.amazonaws.com/0/199678/41e1a098-4ae4-9511-a857-8d6b2779b50b.png)

これで、先程のGoogleDriveの新規ボタンから「その他」にFusion Tablesが追加されて新規作成できるようになりました。
そこから新規作成を選びます。
![sc3_2.png](https://qiita-image-store.s3.amazonaws.com/0/199678/2bd22fe3-a992-a2aa-01ab-f702bb890ef6.png)

新しいテーブルをスプレッドシートから作ることもできますが、今回はCreate empty tableを選びます。
![sc3.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/49001641-f125-f628-aa44-654cf8b71e59.png)

##Fusion Tablesのカラム定義を変更する

Chatwork Webhookから飛んできたイベントを保存する為に、必要なカラムを定義します。
Edit→Change columnsを選択。
![sc4.png](https://qiita-image-store.s3.amazonaws.com/0/199678/90fd7086-120d-5815-8340-64b9e0bfcd1d.png)

以下スクショのように定義します。
注意点が2個あり、

* room_idとmessage_idはNumberではなくTextで。Numberにしてしまうと、桁数が多い関係でjson形式で返すAPIを立てた際、桁が丸められてしまいます（1234567891234567891が1234567891234567000のようになってしまう）。結構これで長く悩みました。
* created_atとupdated_atはスクショのようにFormatを変更しないと、YYYY/MM/DD形式でしか保存できないので注意が必要が必要です。

![sc5.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/040169bc-dcbe-6c92-7166-799bdaa167bd.png)


メニューのEdit→Add rowからデータの追加をしてみてください。
正常に行追加がされていればFusion Tables側の対応は完了です。

##Google Apps Scriptを作成

Google Apps Script（以後GAS）には、GASファイル単体を新規作成する方法（Standalone Script）と、スプレッドシート等に付随したスクリプトとして使う方法（Container Bound Script）があります。

参考：[Google Apps Scriptの開発手法まとめ](https://qiita.com/soundTricker/items/4d04c97c499b22886dfd)

今回は動作確認のしやすさを考慮し、スプレッドシートから利用するContainer Bound Scriptを利用します。

GoogleDriveから新規→Googleスプレッドシートを選択。
ツール→スクリプトエディタを選択します。
![sc6.png](https://qiita-image-store.s3.amazonaws.com/0/199678/a88c582b-3682-a223-1254-2140dd067e4a.png)

出てきたスクリプトエディタに、下記ソースコードをドンとコピペ。

```javascript
var FUSION_TABLES_DOC_ID = 'INSERT_YOUR_FUSION_TABLES_DOC_ID';
var PETTERN = /([\S\s]+)[\s　]#(\S+)/;

function doPost(e) {
  var json = JSON.parse(e.postData.contents);
  var webhookEvent = json.webhook_event;
  var eventType = json.webhook_event_type;

  if (eventType == 'message_created') { // メッセージ新規投稿時
    insertRow(webhookEvent);
  } else if (eventType == 'message_updated') { // メッセージ更新時
    insertOrUpdateRow(webhookEvent);
  }
}

function insertRow(webhookEvent) {
  var matchText = webhookEvent.body.match(PETTERN);
  var body      = matchText && matchText[1] ? esc(matchText[1]) : '';
  var category  = matchText && matchText[2] ? esc(matchText[2]) : '';
  
  if (body && category) { // 本文とハッシュタグの両方がマッチした時のみinsert
    var createdAt = formatDate(webhookEvent.send_time ? webhookEvent.send_time : webhookEvent.update_time);
    var updatedAt = createdAt;
    var sql = "INSERT INTO " + FUSION_TABLES_DOC_ID + " (room_id, message_id, account_id, category, body, created_at, updated_at) " +
                                               " VALUES ('" + webhookEvent.room_id + "', '" + webhookEvent.message_id + "', " + webhookEvent.account_id + ", " +
                                               " '" + category + "', '" + body + "', '" + createdAt + "', '" + updatedAt + "');";
    //debug(sql);
    try {
      var res = FusionTables.Query.sql(sql);
      //debug(res);
    } catch(e) {
      debug('insert error. ' + e);
    }
  } else {
    debug('insert message format invalid.' + webhookEvent.body);
  }
}

function insertOrUpdateRow(webhookEvent) {
  if (!rowExists(webhookEvent.message_id)) { // message_idでのselect結果がnullならinsert
    insertRow(webhookEvent);
  } else { // レコードあったらupdate
    updateRow(webhookEvent);
  }  
}

function rowExists(messageId) {
    var sql = "SELECT message_id FROM " + FUSION_TABLES_DOC_ID + " WHERE message_id = '" + messageId + "' LIMIT 1;";
    try {
      var res = FusionTables.Query.sql(sql);
      return res.rows && res.rows[0];
    } catch(e) {
      debug('select error. ' + e);
  }  
}

function updateRow(webhookEvent) {
  var matchText = webhookEvent.body.match(PETTERN);
  var body      = matchText && matchText[1] ? esc(matchText[1]) : '';
  var category  = matchText && matchText[2] ? esc(matchText[2]) : '';
  
  if (body && category) { // 本文とハッシュタグの両方がマッチした時のみinsert
    var updatedAt = formatDate(webhookEvent.update_time);
    var sql = "UPDATE " + FUSION_TABLES_DOC_ID
              + " SET category = '" + category + "', body = '" + body + "', updated_at = '" + updatedAt
              + "' WHERE message_id = '" + webhookEvent.message_id + "';";
    try {
      var res = FusionTables.Query.sql(sql);
      debug(res);
    } catch(e) {
      debug('update error. ' + e);
    }
  } else {
    debug('update message format invalid.' + webhookEvent.body);
  }
}

function debug(text) {
  var sheet = SpreadsheetApp.getActiveSheet();
  sheet.getRange('A1').setValue(text);  
}

function formatDate(unixTime) {
  var date = new Date(unixTime * 1000);
  return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
}

// ;や'をエスケープし、SQLインジェクションやエラーを防ぐ
function esc(text) {
  return text.replace(/\[\/?(info|title|code)\]/g, "").replace(/'/g, "\\'").replace(/;/g, "\;");
}

```

```
INSERT_YOUR_FUSION_TABLES_DOC_ID
```
ここにはFusion Tablesのdoc_idを入れてください。
doc_idとは、先程作成したFusion TablesのURL欄にある、docid=以降の40桁ぐらいの文字列です。
（#rows:id=1等の文字列は不要なので削る）

**なぜかうまく動かない！**
という時は、debug関数でエラーキャッチをしておりスプレットシートのA1セルにエラー内容が表示されるはずなので、そちらを確認してください。

本来GAS開発の時はログを見ることが多いのですが、postイベントでエラーが起きた時にログがうまく見れなかったりするので、この方法を採用しています。

#GASからFusion Tables操作できるようにする

上記ソースコードをコピペして動かしても、恐らく動かないはずです。

```javascript
FusionTables.Query.sql(sql);
```
この処理はGASからFusion Tablesの拡張をONにしていないと動かない為です。
リソース→Googleの拡張サービスを選び、Fusion Tables APIをONにします。
![sc7.png](https://qiita-image-store.s3.amazonaws.com/0/199678/a5bd8072-f61f-52c4-b6d2-364b857409c2.png)
![sc8.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/f23b00c5-425e-97f8-7086-8b78463c2a87.png)

更に上記スクショの「Google Cloud Platform APIダッシュボード」の文字リンクをクリック。

「APIとサービスの有効化」リンクを押し、遷移先で検索ボックスにFusion Tablesと入力。
![sc9.png](https://qiita-image-store.s3.amazonaws.com/0/199678/5d0e1e61-b4f0-3281-053e-a8408c805735.png)

Fusion Tables APIページに飛んだら「有効にする」をクリック。これで準備完了です。
![sc10.png](https://qiita-image-store.s3.amazonaws.com/0/199678/4916581b-1f55-4243-6f3e-5426536bce9f.png)

#GASをAPI公開

これで問題なければ動くはずなので、このスクリプトをAPIとして公開します。

公開→ウェブ アプリケーションとして導入をクリック。
![sc11.png](https://qiita-image-store.s3.amazonaws.com/0/199678/680080d1-8156-a5b7-729e-ed86668cb782.png)

アプリケーションにアクセスできるユーザーを「全員（匿名ユーザーを含む）」と設定し「導入」を選択。
![sc12.png](https://qiita-image-store.s3.amazonaws.com/0/199678/91c1bcd2-6683-ae6a-68ed-69983bac6cec.png)

このあたり、承認画面が出ます。
リスクの詳細を確認し、導入する際は自己責任でお願いいたします。

![sc15.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/ca7acb07-88fd-73fd-7920-a32912e3b7cd.png)
![sc16.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/82ded938-ab7b-3642-0bc5-9ac480f4fdda.png)

導入が完了するとAPIのURLが表示されます。
このURLにPOST形式でリクエストを送ると、Fusion TablesにInsertされます。
![sc17.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/9108f54c-6262-ce59-2403-b50707554774.png)

#Chatwork Webhookの設定

Chatworkの右上アイコンから「API設定」をクリック。
フリープラン・パーソナルプランの場合はそのまま使えますが、ビジネスプラン・エンタープライズプラン・KDDI ChatWorkの場合は組織管理者への申請が必要となります。

そこをパスしたら、メニューの「Webhook」から「新規作成」を選びます。
そして以下の通り情報を入力します。

![sc18.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/3ab4d00a-5eba-d837-9a21-626a59ac744f.png)
Webhook URLには先程作ったGASで作ったAPIのURLを。
ルームIDは書いてある通り、URLのrid以降の数字です。

Webhookを設定したユーザーが在籍するChatworkの部屋で、以下の通りつぶやいてみます。
![sc20.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/56770bdc-b6e9-5394-e7e8-b3003cb05375.png)

特に問題がなければ、Fusion TablesにデータがInsertされています。
![sc19.PNG](https://qiita-image-store.s3.amazonaws.com/0/199678/574cd35b-5584-a0f3-fef0-2bf424661565.png)

#今後の展望

* view画面の実装
* まだ構想段階で組織の仕組みとして導入していないので、その反応見てアップデート
* ハッシュタグを付けたら「保存したよ」メッセージをChatworkAPIを使い、ストックアクションの見える化＝形骸化防止

等です。
第二弾で記事書くかもしれません。

# お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひとも[エイチームグループ採用ページ](http://www.a-tm.co.jp/recruit/)（[Webエンジニア詳細ページ](http://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-webengineer/)）よりお問い合わせ下さい。

# 明日
明日は@namiitaさんが何か書いてくれるようです。お楽しみに！