LINE Messaging API + AWS + GASでサーバーレスな体温管理

LINEベースで連絡をとっているサークルで、体温を毎日はかり各自Googleスプレッドシートに書き込んで管理していたのですが、回答率が悪く、またLINEでリマインドをすると他の連絡事項などのメッセージが流れていく、、ということが起こっていました。
そこで、LINEの公式アカウントを作り、そこで毎日体温のリマインドをし、そこに返信したらスプシに自動で書き込まれる、というシステムを今まで触れたことがなかったLINE Messaging APIやGASの勉強も兼ねて作ってみました。

#このソリューションのメリット

サークルに限らず会社や他のコミュニティでもそうだと思うのですが、COVID-19の感染が広がる中、メンバーの体温管理が必要な状況となっています。
体温を毎日測ってもらって、フォームで聞くかスプレッドシートに書き込んでもらうという手段が考えられるのですが、少しでもステップが多いと毎日行ってもらうのが厳しくなります。具体的に言うと、URLを送りそれに答えてもらうという形をとると回答率が一気に下がります。そして毎日リマインドをするのも大変で係の心理的負担にもなる上に大事な連絡事項が流れてしまいます。
そこで体温管理のソリューションに必要なことを2つに絞りました。

* 毎日リマインドのラインを個人宛に送る
* リマインドに対して画面を変えずに体温を送信できるようにする

そして個チャの中で送られたメッセージを処理してスプシに書き込むものを作れば、必要な要件を全て満たせているのではないかと言う考えです。

このソリューションのメリットは、**LINEで1タップだけで体温登録でき自動でスプシに書き込んでくれる**こと、そして**毎日個人宛にLINEでリマインドを送信するためグループチャットでの連絡事項が流れない**ことです。

1タップで登録するというところの説明が足りていなかったので実際の画面をお見せします。
![IMG_6158.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/2ada805d-ec20-7304-14de-a967c8a260ce.png)

LINEの公式アカウントのメッセージタイプの中にカードタイプメッセージというものがあったので、それを使ってみました。自分の体温に該当するカードを選んでタップするだけで体温を送信することができます。ボタンクリックした時のアクションは残念ながらLINE Messaging APIでは送ってもらえなかったので、タップしたら「36.3~36.5℃との回答ありがとうございます！」というメッセージがユーザー側からされるように(ちょっと変な感じですが)したら無事Messaging APIにも拾ってもらえたのでそのような構成にしています。なんで0.1℃刻みにしていないのか、という質問については、カードタイプメッセージのカード枚数の上限が9枚だったからです、、今後増えると嬉しいなと思います。


#全体の構成
![forQiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/6514bc40-1a0c-8c70-0238-cc01d583d157.png)

カードをタップした際に出力されるメッセージがLINE Messaging APIを通してAWS LamnbdaにJSONの形で届きます。LambdaでGASに送りたいデータだけを選んで整形して送り、最後にGASで簡単に処理をしてスプシに書き込むという流れです。

LINEからスプシに直接行ってもいいじゃないかという声もあるかもしれないのですが、LINE Messaging APIのWebhook URLを一つしか指定できないことから今後の拡張性を考えるとAWSで一つAPIを作ったほうがいいかなという判断です。

前置きはこのあたりにして実際の作り方に入っていこうと思います。

#作り方
作り方はざっくり5つに分けることができます。

1. LINE公式アカウントを作る
2. AWSのAPI GatewayとLambdaを使ってAPIを作る
3. 作ったAPIとLINEの公式アカウントを結びつける
4. Lambdaからスプシにデータを送る
5. 送られたデータをGASで処理してスプシに出力する

ひとつひとつ説明していこうと思うのですが、長くなりそうなのでわかりやすい参考記事を知っているものについてはそれを貼るなどして省略させていただきます。

##LINE公式アカウント・カードタイプメッセージを作る
<details><summary>公式アカウントの作り方に関しては、[こちら](https://www.linebiz.com/jp/column/technique/20190418-3/)の公式ドキュメントがわかりやすいので是非ご参照下さい。</summary><div>
ドキュメントを読んで公式アカウントを作り、[LINE Official Account Manager](https://www.linebiz.com/jp/login/)の ホーム/友だち追加 をクリックした時に出てくるQRコードをスキャンしてまずは自分の友達登録をしてみて下さい。<br>

今後色々試す時に不便なので。ホーム/応答メッセージ で出てくるタイトルがDefaultのステータスはオフに切り替えておきましょう

次に体温を聞くカードタイプメッセージを作っていこうと思います。[LINE Official Account Manager](https://www.linebiz.com/jp/login/)の ホーム/カードタイプメッセージをクリックして下さい。

↓この画面になったら、作成をクリック
![スクリーンショット 2020-10-23 18.31.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/51713385-e584-5ea1-61a3-01e0bd05d2bf.png)

↓タイトルとカードタイプを聞かれます。タイトルは何でもいいのですが、カードタイプはプロダクトにします。
![スクリーンショット 2020-10-23 18.33.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/5feeb684-154d-cf09-54fa-fc357cbbd661.png)

↓カードの設定は次のようにします。アクションタイプをテキストにして、〇〇℃との回答、ありがとうございます！とするようにして下さい。(℃が大事です、後で出てきます)
![スクリーンショット 2020-10-23 18.36.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/1faa86d3-36d8-cace-ee9f-49fbe94685c6.png)
温度ごとに細かく分けてカードをたくさん作り(9枚まで)、保存します。

↓保存したら、今度は ホーム/メッセージ配信を開いて下さい。作成をクリックです。
![スクリーンショット 2020-10-23 18.39.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/b5d15fa1-681d-7dfa-e04a-d745d301c5fb.png)

希望の配信日時、メッセージはカードタイプメッセージを選び、先ほど作ったものを選択して下さい。
![スクリーンショット 2020-10-23 18.41.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/b1f6984d-1a6e-d41e-9645-655bf7e0e0a1.png)

それが配信され、カードをタップすると「〇〇℃との回答ありがとうございます！」というメッセージが見えたらセクション1は成功です。

</div>
</details>

##AWSのAPI GatewayとLambdaを使ってAPIを作る
<details><summary>Webhookの設定上オープンAPIが必要です。API GatewayとLambdaを使って作っていきます。</summary><div>
まずAWSのアカウントを持っていない方は、[こちら](https://aws.amazon.com/jp/register-flow/)を参考にアカウントを開設して下さい。<br>
※AWSマネジメントコンソールのUIは2020/10/23のものです
↓AWSのアカウントを作ったら、まずマネジメントコンソールでLambdaを検索してください。
![スクリーンショット 2020-10-23 14.38.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/d8ba061b-f753-a178-6c5e-61308c56befa.png)
↓左側のメニューで関数が選択されているのを確認し、「関数の作成」をクリック
![スクリーンショット 2020-10-23 14.40.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/e3a4828b-99dc-8f95-3cf8-8e6476ec45c3.png)
↓設計図の使用、microservice-http-endpointをクリック、右下の設定をクリックして下さい。
![スクリーンショット 2020-10-23 14.44.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/81b4298c-c200-c0bb-ea96-5707c7846b78.png)
↓画面の様に設定をします。関数名・ロール名はご自由に設定して下さい。関数のコードはそのままにして、右下の関数の作成をクリック
![us-west-2.console.aws.amazon.com_lambda_home_region=us-west-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/4cd05261-be8b-41db-9ff0-6be43b6450b1.png)

Lambdaのデフォルトの関数コードをとりあえず以下の様に変えます(あとでもう一度変えます)

```
const AWS = require('aws-sdk');

exports.handler = (event, context) => {
    //console.log('Received event:', JSON.stringify(event, null, 2));
    let body;
    let statusCode = '200';
    const headers = {
        'Content-Type': 'application/json',
    };

    try {
        switch (event.httpMethod) {
            case 'DELETE':
                body = {"status":"delete success"};
                break;
            case 'GET':
                body = {"status":"get success"};
                break;
            case 'POST':
                body = {"status":"post success"};
                break;
            case 'PUT':
                body = {"status":"put success"};
                break;
            default:
                throw new Error(`Unsupported method "${event.httpMethod}"`);
        }
    } catch (err) {
        statusCode = '400';
        body = err.message;
    } finally {
        body = JSON.stringify(body);
    }

    return {
        statusCode,
        body,
        headers,
    };
};
```
今回はDynamoDBを使うことはないのでこの様に書き換えます。
Deployを押し忘れないようにして下さい。

次にAPI Gatewayの設定をしていきます。
↓API Gatewayを開くと「先ほど作った関数-API」という名前のAPIが既にあることを確認できます。自分の作ったAPIをクリックして下さい。
![スクリーンショット 2020-10-23 15.23.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/efd52b9d-5c1d-9185-da32-0dfae84f07bb.png)
↓するとこの様な画面になるはずです。真ん中のあたりにあるアクションをクリックして、APIのデプロイをして下さい。
![スクリーンショット 2020-10-23 15.26.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/ffa83e0f-25ea-98db-a799-11f02a08e051.png)
↓画面が変わった後のURLの呼び出しのところに書いてあるURLをコピーしてください。このように、~/defaultになっているはずです。**この後に、リソースのところにあるメソッドの名前を入れてください。**全体では、https://hogehoge.amazonaws.com/default/myFunction20201023 のようになっているはずです
![スクリーンショット 2020-10-23 15.40.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/63d7b2a6-f068-022d-cda0-e26c91d352f7.png)
これでLINE公式アカウントやスプレッドシートとやりとりするAPIが完成しました。
簡潔にするために、本当はもっと設定した方がいいところなど省略しているのですが、この通りにやっていただいたらとりあえず動くものはできます。
</div></details>
##作ったAPIとLINEの公式アカウントを結びつける
<details><summary>[こちら](https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-oa-manager)をみながらLINE Messaging APIのアクティベートをし、LINE Developersコンソールに登録していない場合は登録し、[こちら](https://developers.line.biz/ja/docs/messaging-api/building-bot/)を見て自分の公式アカウントに先ほどコピーしておいたURLをWebhook URLとして登録します。</summary><div>
↓公式アカウントを作った時にも閲覧した[LINE Official Account Manager](https://www.linebiz.com/jp/login/)を開き、自分のアカウントを選択したら、右上にある「設定」をクリックして下さい。
![スクリーンショット 2020-10-23 16.08.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/c0fedb9d-a9b5-35af-3788-04ef08135e7c.png)
左のメニューの中に「Messaging API」があるので、そこをクリックし、Messaging APIを有効にして下さい。Messaging APIのチャネルの設定は、[LINE Developersコンソール](https://developers.line.biz/console/)で行います。 LINE Official Account Managerにログインするときに使っているLINEアカウントで、LINE Developersコンソールにログインします。

チャネルができたらWebhook URLを設定していこうと思います。[LINE Developersコンソール](https://developers.line.biz/console/)で自分の作ったチャネルを選択し、Messaging APIを選択して下さい。

↓少し下にスクロールすると、このようにWebhookURLの下にEditという文字があるのでそれをクリックして下さい。
URLを入力するところがあるので、そこに先ほど用意してURLを入力します。/defaultの後にメソッド名を付け加えるのを忘れないようにして下さい。verifyを押し、200レスポンスが返ってくるのを確認して下さい。返ってこなかった時にはpostmanなどを使って原因を確認していきます。
![スクリーンショット 2020-10-23 16.22.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/70b65ef8-c6f7-19fa-9558-9548d040d392.png)

Verifyのボタンを押した際に200レスポンスが返ってくることが確認できたらこのセクションは成功です。

ここまで設定したことで、LINEの公式アカウントでユーザーがメッセージを送ったり、新しく公式アカウントを追加した際にLambdaにJSONデータが送られるようになりました！この調子で次は必要なデータを抽出してスプシに送る用意をしましょう。
</div></details>

##Lambdaからスプシにデータを送る
<details><summary>新しくスプシを作り、スクリプトエディタで簡単な関数を作り、Web APIとして公開し、Lambdaからデータを送るように設定します。</summary>
<div>
GASに関しては[この連載](https://tonari-it.com/gas-script-editor/)がとても丁寧で非常に助かったので初めてGASを触る方はこれを片っ端から進めていくのがおすすめです。

まずはスプシを一枚作ってみて下さい。この時にある方はG Suiteアカウントで作成すると後の権限の設定が簡単になるのでそちらを使った方がいいと思います。

↓上のメニューの中の「ツール」をクリック、その中にある「スクリプトエディタ」をクリックして下さい。
![スクリーンショット 2020-10-23 17.08.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/8d425f9b-efe8-be7c-1fd7-c04a4acf1bf8.png)

↓このようにエディタが開かれます。
![スクリーンショット 2020-10-23 17.22.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/87b4d2c0-4265-026c-03b8-16449d8c5812.png)

ここに、

```
function doPost() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = SpreadsheetApp.getActiveSheet();
  const range = sheet.getRange(1,1);
  range.setValue("hello world");
}
```

このようにコードを書きます。これはスプシのA1にhello worldと出力するコードです。cmd+sで保存、プロジェクト名を記入、cmd+rで実行です。許可を求められるのでG Suiteアカウントの方は許可を確認と押して下さい。無料アカウントの場合は、もう少し複雑です。[この記事](https://tonari-it.com/gas-script-approval/#toc10)をご参照下さい。

A1にhello worldと書き込まれたのを確認できたら、次はこれをweb APIとして公開してみます。

↓公開→ウェブアプリケーションとして導入をクリックして下さい。
![スクリーンショット 2020-10-23 17.34.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/c9de26a2-2122-e1e5-d857-65b00fc8c380.png)

↓このようなモーダルが出てきたら、下のように設定し、Deployをクリック
![スクリーンショット 2020-10-23 17.37.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/66240694-f712-072c-888b-670354a5cfb8.png)

URLが表示されるので大切に取っておきましょう。

次はLambdaの関数コードをもう一度いじっていきます。
[AWSマネジメントコンソール](https://aws.amazon.com/jp/console/)に移動です。
先ほどいじったLambdaをもう一度開き、今度は次のようにコードを変更してみて下さい。

```
const AWS = require('aws-sdk');
const https = require("https");

exports.handler = (event, context) => {
    console.log('Received event:', JSON.stringify(event, null, 2));
    let body;
    let statusCode = '200';
    const headers = {
        'Content-Type': 'application/json',
    };
    
    try {
        switch (event.httpMethod) {
            case 'DELETE':
                body = {"status":"delete success"};
                break;
            case 'GET':
                body = {"status":"get success"};
                break;
            case 'POST':
                body = {"status":"post success"};
                //ここからスプシに投稿するコード
        
                const json = JSON.parse(event.body);
             
                var postMessage = {
                    'text': json.events[0].message.text,
                    'timestamp': json.events[0].timestamp,
                    'userId': json.events[0].source.userId
                }
                
                var postDataStr = JSON.stringify(postMessage);
                console.log("postData↓");
                console.log(postDataStr);
                console.log("postData↑");
                let options = {
                    host: 'script.google.com',
                    path: '/先ほどコピーしたURLのscript.google.com/と/execに挟まれている部分/exec',
                    port: 443,
                    method: 'POST',
                    headers: {
                                    'Content-Type': 'application/json',
                                    'Content-Length': Buffer.byteLength(postDataStr)
                    }
                };
                var post_req = https.request(options, function(res) {
                    console.log("https requested")
                    res.setEncoding('utf8');
                    res.on('data', function (chunk) {
                      console.log('Response: ' + chunk);
                      context.succeed();
                    });
                    res.on('error', function (e) {
                    console.log("Got error: " + e.message);
                    context.done(null, 'FAILURE');
                    });
                 });
                post_req.write(postDataStr);
                post_req.end();
                //ここまでスプシに投稿するコード
                
                break;
            case 'PUT':
                body = {"status":"put success"};
                break;
            default:
                throw new Error(`Unsupported method "${event.httpMethod}"`);
        }
    } catch (err) {
        statusCode = '400';
        body = err.message;
    } finally {
        body = JSON.stringify(body);
    }

    return {
        statusCode,
        body,
        headers,
    };
};
```

関数をDeployするのを忘れないようにして下さい。関数がDeployできたら、LINEで公式アカウントに何かメッセージを送ってみて、スプシにhello worldが書き込まれるのが確認できたらこのセクションは成功です。

</div></details>

##送られたデータをGASで処理してスプシに出力する
<details><summary>スプシに送られたユーザーID、timestamp、メッセージをGASの中で処理して、スプシに出力していきます。</summary><div>
私の場合は既にスプシに名前の順番が決まっていて、それがラインの表示名とは違う名前での並びだったので、最初のメンバーの名前とLINEのユーザーIDの照らし合わせは手作業でやってしまったのですが、人数が多い場合は、[こちら](https://developers.line.biz/ja/reference/messaging-api/#get-profile)のようにプロフィール情報をゲットできるAPIがあるのでそれも組み合わせるとより効率的にできるのではないかと思います。

先ほどのスプシのスクリプトエディタで、

```
function doPost(e) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = SpreadsheetApp.getActiveSheet();
  const member = {自分の団体の人数(わからない場合は大きめの値を入力するといいと思います)}
  var jsonString = e.postData.getDataAsString();
  var data = JSON.parse(jsonString);
  
  var userId = data.userId;
  var time = data.timestamp+32400000; //日本時間に合わせています
  var dateRow = Math.floor(time/86400000)-18521(18521は適当な値に変更して下さい。); //epoch時間がミリセカンド単位になっていたので1日単位になるように割り、日時が希望する列に合うように1970年1月1日からの日数を計算して引きます。
  var text = data.text;
  var temperature = text.split("℃"); //メッセージを℃の文字で分け、数字の部分だけがスプシに書き込まれるようにします。
  const peopleRange = sheet.getRange(3, 3, member, 1);//UserIdを3列目、上から3行目から書き始めた場合です。
  const peopleValues = peopleRange.getValues()
  var peopleArrayNumber;  

  for(let i=1; i<member; i++){
    if(String(peopleValues[i])===userId){
      peopleArrayNumber = i+3 //上から3行目から始まっているためです。
    }   
  }
  
  const range = sheet.getRange(peopleArrayNumber,dateRow);
  if(text.match("℃")){
    range.setValue(temperature);
  }
}
```
このように変更して、保存、公開をして下さい。
最初に作ったカードタイプメッセージを送信してタップし、このように書き込まれていったら成功です。
![スクリーンショット 2020-10-23 18.19.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/499660/1a3e39cc-e928-0388-3daf-b226401d0d77.png)
注意点としては、ユーザーIdで検索をかけているので、ユーザーIdだけは最初に用意しておかなければいけないです。今後改善していってその部分は直していこうと思います。
</div>
</details>

長い記事に最後までお付き合いいただきありがとうございました！このソリューションがより多くの方のお役に立てたら幸いです。