GASとMessaging APIでメッセージの送受信をしてみた。

#はじめに
本編はGASをよく知らない（JSも知らない。）私が、
のりで書いてみたものなので正常動作は保証できません。
検証の際は、自己責任でお願いいたします。
#ある休みのこと
コロナウイルスの感染拡大を受けて、暇になった私は、
スプレッドシートで図を書いていた。
「絵を書きやすくなるツールないかなぁ」
と思い、ツールの項目を押してみた。![SnapCrab_No-0021.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/d9d6bdef-294e-9c6c-6131-086e9988a2a1.jpeg)
すると、スクリプトエディタという機能があった。
開いてみるとそこには、
**コードエディタ**があった。（そりゃそうだ）
#目的
今回の目的は、暇となった時間をつぶすため、
[Google Apps Script](https://script.google.com/home)
でMessaging APIを使ってメッセージを送受信しようという企画です。
普段はPythonで運営していますが、最近電気代高いので、
Google先輩のサーバーを借りてしまおうと言う魂胆です。

基本的に、GASを知ってる人にとっては、
**見たことある、コード雑、エラー処理書け**で
終わってしまいそうですが、
まあ、変な書き方もあったもんだと思って見てください。

そして、あんまり触れたことないけど、
Messaging API関係触れたいなって人は、
自前で環境のいらないGASで試してみてほしいと思います。
それでは、設定項目から、確認していきましょう。
#1.初期設定（Messaging API側）
1 [Line developers](https://developers.line.biz/ja/)に行き自分のラインアカウントでログインをする。
2[コンソール画面](https://developers.line.biz/console/)のコンソール（ホーム）よりプロバイダーがない方は作成します。
（名称だけだったはず？）
ある方は、自分が管理者のプロバイダーを選んでおきます。
3新規チャンネルを押して、メッセージングAPIを選択。
画面に従い登録します。
4作成したら、そのチャンネルをプロバイダー選択後の一覧から選択します。
5メッセージングAPI設定を開き、応答メッセージを無効、
ほかはお好みで設定します。
6チャンネルアクセストークンを発行ボタンを押し発行して、コピーします。
これで一旦終わりですが、まだこのページは使うのでキープします。
#2.初期設定（GAS側）
1[Google　drive](https://drive.google.com/)などから、新規のスプレッドシートを作成します。
2画面上部の”ツール”からスクリプトエディタを開きます。
3開くと![SnapCrab_No-0022.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/277b642e-7f13-0011-7b5b-3480e7a7477f.jpeg)
こんなのが出てくるので、
とりあえず全部消しちゃいます。
終わったら”ctrl＋S”で保存します。
4そしたらとりあえず、最低限のコードを書き揃えます。

```js
const CHANNEL_ACCESS_TOKEN = 'さっきのアクセストークン'; 
const line_endpoint = 'https://api.line.me/v2/bot/message/reply';
const url = "https://api.line.me/v2/bot/message/push";
```
1行目はアクセストークンを入力し、（あまり良くない気もしますが）
2~3ではAPIのエンドポイントを指定しておきます。
#3.初心者（＝私）にとって謎ポイント①
今回メッセージを受け取りには、doPostという関数を用意して受け取ります。

```js
function doPost(e) {
//処理書き込み
}
```
私はeを最初忘れて**動かねぇ**となりましたのでご注意ください。
ちなみに勝手に解釈しているので、知りたいのですが、
doPost()関数は送られてきたデータの受取場として考えいるのですが、
どうなんでしょうか。
詳しく知りたい方は、多分ググれば出ます。
#4.構成
とりあえずコードを御覧ください

```js
const CHANNEL_ACCESS_TOKEN = 'さっきのアクセストークン'; 
const line_endpoint = 'https://api.line.me/v2/bot/message/reply';
const url = "https://api.line.me/v2/bot/message/push";
function doPost(e) {
  var date = new Date();
  var json = JSON.parse(e.postData.contents);
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('talk');
  var setsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('setting');
  var lastRow1 = setsheet.getRange(1, 2).getValue();
  var userId=json.events[0].source.userId;
  var groupId =json.events[0].source.groupId;
  var roomId=json.events[0].source.roomId;
  var content_type=json.events[0].message.type;
  if (content_type==="text"){
    var message = json.events[0].message.text;
  }else{
    var message=content_type; 
  }
  
  sheet.getRange(lastRow1, 1).setValue(Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd|hh:mm:ss'));
  sheet.getRange(lastRow1, 2).setValue(userdata_get(userId));
  send = emo(message);
  sheet.getRange(lastRow1, 4).setValue(groupId);
  if (sheet.getRange(lastRow1, 4).isBlank()){
    sheet.getRange(lastRow1, 4).setValue(roomId);
  }
  if (sheet.getRange(lastRow1, 4).isBlank()){
    sheet.getRange(lastRow1, 4).setValue(userId);
  }
}

function userdata_get(userId){
  var profile_point="https://api.line.me/v2/bot/profile/"+userId
  var response = UrlFetchApp.fetch(profile_point, {
    'headers': {
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
    }
  });
  return JSON.parse(response.getContentText()).displayName;
}

function push_message(){
  Utilities.sleep(3000);
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('talk');
  var setsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('setting');
  var lastRow1 = setsheet.getRange(1, 2).getValue();
  var lastRow2 = setsheet.getRange(2, 2).getValue();
  var least_point = lastRow1 - 1;
  var form = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('form');
  var sendroomid=sheet.getRange(least_point, 4).getValue();
  var content = form.getRange(lastRow2, 2).getValue();
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
  };
  var postData = {
    "to" : sendroomid,
    "messages" : [
      {
        'type':'text',
        'text':content,
      }
    ]
  };
  
  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };
  UrlFetchApp.fetch(url, options);
}
```
こりゃきたねぇ。というわけでこっから解説していきます。
一応のリンク集



#5~13行目
ここでは、APIから送られてきたJSONデータをばらして要素別に宣言しています。
各変数ごとに見ていきます。
data
こいつはAPIからではなく普通に現在時刻を取得しています。日付型です。
json
確か、APIからのJSONデータを取得してパース？するらしいです。
よくわかんないけど、みんな書いてるんで書いときます。
sheet,setsheetはどちらもスプレッドシートをシートごとに呼び出してます。
lastRow1は設定シート（setting）のB1セルの中身です。
B1セルには関数として、
`=COUNTA(talk!C:C)+1`
が書かれています。
これでメッセージを何件受け取ったか数えています。
userId,groupId,roomIdではAPIからのデータにある、
個人識別のためのuserIdと送信元のgroupId,roomIdです。
ちなみに1:1トークでは、ユーザーIDが送信元になります。
（だから人狼ボットとかは、
夜のときなどのメッセージを個人に飛ばせるわけか。）
content_typeでは送られた内容の種類を特定します。
詳しくはリファレンスをどうぞ。
（https://developers.line.biz/ja/reference/messaging-api/）
#14~30行目
ここから記録処理です。
まず14~18行目でテキストメッセージかどうか判断します。
このコードで数少ないエラー回避文です。
写真などでは、メッセージにテキストなどは含まれないため、
テキストメッセージのときは、
JSONの中にあるメッセージの文章を、取得します。
ないときは、代わりにコンテンツの種類を書いておきます。
20~30行目は、セルへの書き込みです。
一番左のA列から、日付、ユーザー名、内容、送信元の順で書きます。
24行目からは、送信元を空にしないために、
グループから送られてなかったら、ルームを確認、
ルームでもなかったらユーザーIDを書いておきます。
（この処理をいらなくすると今回のプログラムが多分動かなくなります。）
#userdata_get(userId)関数
```js
function userdata_get(userId){
  var profile_point="https://api.line.me/v2/bot/profile/"+userId
  var response = UrlFetchApp.fetch(profile_point, {
    'headers': {
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
    }
  });
  return JSON.parse(response.getContentText()).displayName;
}
```

この関数では、ユーザーの表示名を取得します。
ただ、欠点としては、友達でないユーザーには対応しておらず、
処理が止まってしまいますのでご注意ください。
まず、2行目でユーザーのプロフィールを取得するエンドポイントを宣言します。
3~7行目で、データを取得します。
取得するにはユーザーIDもいりますのでご注意ください。（URLの末尾）
最後に、表示名を返します。
簡単ですが、汎用性は高そうな気はします。
#push_message()関数にふれる前に...
このあと触れていきますが先にシートの方に書き込む内容についてざっくりと話していきます。
文中の中でシートが2枚宣言していましたが
今回は、記録するシートとしてtalk
設定書き込み用としてsettingを用意しています。
またこのあとフォーム作成をしますが、それの回答が書き込まれるシートをformとしています。
まずtalkから見ます。
![SnapCrab_No-0024.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/23c3e3d7-9b5f-34c1-1e41-16fedd4bb68c.jpeg)
一行目に左から、
time,username,content,groupIdと書いてあり、その下に記録されます。
一番右のURLは欲しい人は書いてください。
次にsettingシートですが、
![SnapCrab_No-0023.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/b0d0041c-1ef3-3bee-5410-d8184da442bd.jpeg)
B1には=COUNTA(talk!C:C)+1
B2には=COUNTA(form!B:B)
と記述します、あとは書かなくても動きはします。
B1の関数では現在書き込まれている数＋1しているので次のセルへの書き込み参照に用いられます。
B2ではフォームの回答数を取得しているので最新の回答のある行を取得できます。
#フォームの作成
画面上のツールよりフォームの作成を選び、こんな感じのフォームを作ります。
![SnapCrab_No-0025.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/db0bc163-ec24-452c-88f4-0a9358f3d53b.jpeg)
あとは送信ボタンを押しURLを取得しておいてください。
このフォームが送信フィールドになります。
そうすると勝手にスプレッドシートに回答用シートができてるのでformとリネームしておいてください。
#動作検証の準備（受信編）
受信のためにスクリプトエディター内の公開ボタンからウェブアプリケーションとして導入を押します。
![SnapCrab_No-0026.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/130cfa23-4d1b-93bb-b607-8fe63c6a4550.jpeg)
こんな画面が出たら、
project versionを**new**(毎回必ずこれにしてください。)
あとは画面と同じ設定にして青い方を押してください。
そうすると、認証が必要になるので、認証します。
（途中でGoogleに確認されていないアプリと出ますが無視してすすめます。）
そして
This project is now deployed as a web app.
のようなメッセージが出ればいいのでCurrent web app URL:
をコピーします。

そしたら、だいぶ前に開きっぱなしのLinedevelopersのページに、
webhookURLに先程のURLをコピーし、
webhookの利用を有効にします。

そしたら上にあるQRコードをスマホで読み取り友達登録して、適当に打ち込めば、
多分書き込んでくれます。（時間がかかるときもある）
注意というか謎というかとしては、
メッセージを連投されるとうまくかきこめないようです。
（秒間3つでだめでした。）
ここはいつか直したいですね。
#push_message()関数について
おまたせしましたそれでは、この関数について解説していきます。
コードを持ってきます。

```js

function push_message(){
  Utilities.sleep(3000);
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('talk');
  var setsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('setting');
  var lastRow1 = setsheet.getRange(1, 2).getValue();
  var lastRow2 = setsheet.getRange(2, 2).getValue();
  var least_point = lastRow1 - 1;
  var form = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('form');
  var sendroomid=sheet.getRange(least_point, 4).getValue();
  var content = form.getRange(lastRow2, 2).getValue();
      var headers = {
        "Content-Type" : "application/json; charset=UTF-8",
        'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
      };
      var postData = {
        "to" : sendroomid,
        "messages" : [
          {
            'type':'text',
            'text':content,
          }
        ]
      };
      
      var options = {
        "method" : "post",
        "headers" : headers,
        "payload" : JSON.stringify(postData)
      };
      UrlFetchApp.fetch(url, options);
    }
```
この関数は、ちょっとGASを知ってる方ならわかると思いますが、
こいつはdoPost関数には含まれていません。
（は、なんで？と思った方に一応分かる範囲で今回のコードを説明すると、
Messaging APIには大きく2つのメッセージ送信方法があり、
一方は応答メッセージといい、期限付きのトークンを必要とするものですが、代わりに、メッセージの送信のコストは掛かりません。
そして今回使っているものは、プッシュメッセージといい、
期限付きトークン不要で送信できますが、
今回使うプランでは月1000？までしか送信できないため、
短時間で返信できる場合は応答メッセージを使います。）
ではどうやって実行するのかという話は最後にしますのでとりあえずコードを見ていきましょう。

2行目ではフォームの書き込みを待機させています。
sleep()関数みたいです。
3~27行目では必要なものを宣言しています。
sheet,setsheetは先程同様シートの宣言をしています。
lastRow1,2はtalkとformの最終行(talkは+1)を取得しています。
least_pointはlastRow1から1引いてtalkの最終行を取得
formで回答用シートの取得
sendroomidでは最後に受信したトークルーム等のIDを取得します。
ここの値を任意にすれば、好きなところにメッセージを送れます。
contentはフォームの回答に書かれたメッセージの内容を取得します。
headers,postData,optionsは送信のためのデータを型にはめておきます。
そして送信のためにUrlFetchApp.fetchで送信処理をします。
本来なら、ここでレスポンスを書いてエラー処理すべきですが、
お試しなのでスルーします。
#完成...でもUIほしくない？
これでコード自身はあらかた完成なのですが、
いちいちスプレッドシート見るのはめんどいと思ったのでUIを作ります。
ただ、面倒くさいので今回は、既存サービスを使ってショートカットです。
それがスプレッドシートを使って、簡単なwebアプリが作れる。
[glideapps](https://www.glideapps.com/)
です。
今回は簡単にですがこれでUIを作っていきます。
#UIを作ろう。
1.まず先程のサイトにアクセスし
サインアップしましょう
2サインアップ完了後、
![SnapCrab_No-0027.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/26a3f951-a9ed-7724-4833-7eb4beb4dd64.jpeg)
create appからfrom Google sheetを選び、先程まで作っていたファイルを選択します。
3
![SnapCrab_No-0028.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/647db827-e7ee-00b8-5085-a343a81e1d13.jpeg)
すると編集画面が出るのでお好みで編集しちゃってください。
機能としては１/500のしたのアイコンから、
レイヤー（一画面の中でどんなふうにシートのデータを出すか）
タブ（シートごとにプレビュー画面の下の方にバーを出せる。
なお右のChatはこのアプリの機能で、ここでメールアドレス認証することで、
コメントできる。自分は緊急連絡ように作成。）
データ（ここではシートのデータがどんなふうか見れる。編集も可能）
プレビュー（アンドロイ、IOSでの表示切り替え）
設定（アクセスを制限できる設定など。アプリ名などもここで変更可能。
アクセス制限は必須。）
あと２つは、上が、スプレッドシートに移動、下がシートの更新です。
なお、現在ではフリープランでは、500行までしか読み込めなくなっていました。
（でも、軽く触るならPROにする理由はないかな）
#最終調整
最後にトリガーを設定して、フォームの送信時にpush_message()を実行できるようにします。
スクリプトエディタの編集＞現在のプロジェクトのトリガーを選び、
トリガーの追加をします。
![SnapCrab_No-0029.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498105/d5e433d5-affc-955f-8759-eb8fb81906ea.jpeg)
こんな感じに設定してやれば、OKです。
これでおしまいです。
お疲れさまでした。
#途中で出てきたURLの列って結局何さ。
URLの列はウェブアプリ化した際に、そこから直接、メッセージの送信フォームに行くために作りました。
なくて全然問題ないです。
#おわりに
今回は0からでもなんとかできました。
（エラー処理がないって？そりゃそうだ。）
実用うんぬんではなくなんとなく触ってみたい人のきっかけになれば幸いです。
それでは、コードだけ貼っておわります。

```js
const CHANNEL_ACCESS_TOKEN = 'さっきのアクセストークン'; 
const line_endpoint = 'https://api.line.me/v2/bot/message/reply';
const url = "https://api.line.me/v2/bot/message/push";
function doPost(e) {
  var date = new Date();
  var json = JSON.parse(e.postData.contents);
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('talk');
  var setsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('setting');
  var lastRow1 = setsheet.getRange(1, 2).getValue();
  var userId=json.events[0].source.userId;
  var groupId =json.events[0].source.groupId;
  var roomId=json.events[0].source.roomId;
   var content_type=json.events[0].message.type;
  if (content_type==="text"){
    var message = json.events[0].message.text;
  }else{
    var message=content_type; 
  }
  
  sheet.getRange(lastRow1, 1).setValue(Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy/MM/dd|hh:mm:ss'));
  sheet.getRange(lastRow1, 2).setValue(userdata_get(userId));
  sheet.getRange(lastRow1, 3).setValue(message);
  sheet.getRange(lastRow1, 4).setValue(groupId);
  if (sheet.getRange(lastRow1, 4).isBlank()){
  sheet.getRange(lastRow1, 4).setValue(roomId);
    }
  if (sheet.getRange(lastRow1, 4).isBlank()){
  sheet.getRange(lastRow1, 4).setValue(userId);
  }
}

function userdata_get(userId){
  var profile_point="https://api.line.me/v2/bot/profile/"+userId
  var response = UrlFetchApp.fetch(profile_point, {
    'headers': {
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
    }
  });
  return JSON.parse(response.getContentText()).displayName;
}

function push_message(){
  Utilities.sleep(3000);
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('talk');
  var setsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('setting');
  var lastRow1 = setsheet.getRange(1, 2).getValue();
  var lastRow2 = setsheet.getRange(2, 2).getValue();
  var least_point = lastRow1 - 1;
  var form = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('form');
  var sendroomid=sheet.getRange(least_point, 4).getValue();
  var content = form.getRange(lastRow2, 2).getValue();
      var headers = {
        "Content-Type" : "application/json; charset=UTF-8",
        'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
      };
      var postData = {
        "to" : sendroomid,
        "messages" : [
          {
            'type':'text',
            'text':content,
          }
        ]
      };
      
      var options = {
        "method" : "post",
        "headers" : headers,
        "payload" : JSON.stringify(postData)
      };
      UrlFetchApp.fetch(url, options);
    }
```

#（個人的）悲報
LineがGoogleAssistantに対応したらしい。
(https://www.watch.impress.co.jp/docs/news/1241328.html)