LINE BOTとチャットモード併用が行えないので擬似チャットモードを実装してみた

# 始めに
　LINEの公式アカウントを使用して顧客とのやり取りを自動化したい要望って結構多いですよね。でもLINEの公式アカウントの仕様上、BOTモード使用時はチャットモードが使用できず、だからといって都度BOTモードとチャットモードを切り替えるのは現実的ではない。
　ただし、簡単にググった限り、チャットとBOTを併用できるようなサービスって既に存在してますよね（チャンネルトークっていうサービスが該当しそう）。でもそれも月額使用料かかるじゃん...、と。
　というわけで、できるだけ手出しを少なくしてBOT×チャットを実現してみようという試みなわけです。

　それに、LINE BOTってGAS使えば簡単に実装できて、かつなんか製品が出来上がった感じがして気持ちいいですよね。

# 目次
1.使用する技術
2.動作機序を検討する
3.ソースコード
4.実行結果
5.まとめ

## 1.使用する技術
・LINE Messaging API（当然ですね）
　お金をかけたくないので、フリープランを前提に考えています。
・GAS
　今回考えている程度の機能を実装するだけならGASで十分ですね（つーかこれが限界）
　お金をかけたくないので、当然フリーのgoogleアカウントを前提に考えています。

## 2.動作機序を検討する
### 1)そもそもLINE公式アカウントのチャットモードとは？
　LINE公式アカウントマネージャーで、アカウントページに飛ぶと、ページ上部に「チャット」という欄が選択できる。実際にそれを選択すると各ユーザーと手動で通常のLINE使用時と同様に個別のトークが行える。
![C39FFF97-E8A1-4541-876D-6588A2BC35D3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682592/c9d8005d-fbca-7160-8648-e4dd5af6b072.jpeg)

　ところが、LINE DeveloperのページでBOTモードを選択中はチャットモードの使用ができない旨の注意書きが...。
　普通に考えれば不要ですけどね。私も「BOTとチャットって併用できない？」と知り合いの個人事業主に聞かれて初めてそういう需要が存在することを知りました。
![26EB5E7D-C3F5-48A3-9525-396F611D854A.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682592/50926ae0-f5af-8978-a187-7c6b667b8c2f.jpeg)

### 2)使用可能な機能を検討する
　BOTとチャットが併用できないということが改めて分かったので、使用可能なAPIの機能で「擬似チャット」が行えないか考えます。
　別に私もLINEのプロじゃないので、LINE Messaging APIでメッセージを送る機能と言えば知っているのは下記ぐらい。
・リプライメッセージ
・プッシュメッセージ
　→プッシュ（1対1）
　→マルチキャスト（1対多）
　→ナローキャスト（1対多）
　→ブロードキャスト（1対多）

　リプライメッセージは、送ってきたメッセージ個別のリプライトークンを使用して返事をする機能です。リプライトークンは確か3分くらいの間は有効なので、「sleepさせて3分以内に返事を書かせるか？」と最初考えましたが、処理が面倒だし、3分越しちゃったらどうすんのということで却下。
　プッシュメッセージには1対1と1対多でメッセージを送る機能があるけれど、今回は個別のチャットが目的なので1対1のプッシュメッセージを採用。

### 3)処理の流れを検討する
　使用する機能も決定したことなので、処理の流れを検討します。
　プッシュメッセージを使用したことがある人はわかると思いますが、プッシュメッセージには送る相手のLINE IDが必要です。それを指定して、BOT→特定ユーザーのメッセージを送ることができます。
　でもやりたいことは、管理者ユーザー⇄特定ユーザーなので、プッシュメッセージを使用するとなると管理者ユーザー⇄BOT⇄特定ユーザーとなります。
　なんか凄い無駄が多い気がするけれど、やっぱこういうのってコーディングの簡素さも重要じゃないですか？（言い訳）

　なので、
①特定ユーザーからメッセージを受け取ったBOTは、メッセージと一緒にユーザーIDを管理者ユーザー宛に転送する。
②管理者ユーザーは、それに返信する形でメッセージと送り先のユーザーIDをBOT宛に送る。
③管理者ユーザーから返信メッセージとユーザーIDを受け取ったBOTは、ユーザーIDを元に返信メッセージを特定ユーザーに再度転送する。
という処理にすることに決定。

　これならコーディングも簡単簡単。いやあ...でもさあ、これ結構問題点あるでしょ...。事項に続く。

### 4)懸念される問題点
①あくまで「擬似チャットモード」であり、管理者ユーザーは一つのトークルームで複数のユーザーとやり取りを行うことになるので、かなり見づらいのでは？
②ユーザーからのリプライを起点としているので、管理者ユーザーからユーザーに連絡を取りづらい。でもこれは送りづらいだけで送れないことはないので、まあヨシッ。
③プッシュメッセージはLINEの有償メッセージというものに該当します。LINEのフリープランだと月間1,000通までは無料で使用できますが、それを超えるとプッシュメッセージが送れなくなります。リプライメッセージはLINE側での制限はないですが、とにかく使う前にどのくらいのやり取りをするのかというシミュレーションは必須です。LINEのアカウントをライトプラン（月に5,000円払って、15,000通/月送り放題）に変更して上限を上げることはできますが、無料で機能を実装したいという前提が崩れるのでダメじゃん！！
④GASを使用する場合にはどの場合にでも注意が必要ですが、gasのUrlFetchApp機能はアカウントにつき、20,000回/日までという制限があります。普通は大して気にする必要はないですが、一つのアカウントでいくつかのBOTを起動している場合や、何らかの悪戯を受けた場合はこれを超えることも考えられます。この制限を超える場合は、LINEの1,000通と違ってプッシュメッセージが送れなくなるだけでなく、リプライメッセージも停止してしまいます。つまりBOTが停止する。
⑤上記の通り、悪意のあるユーザーに悪戯を受けた場合に非常に弱いです。仮に、LINEのライトプランを使用して、gasの制限一杯に攻撃を受けた場合、
・gasの上限により20,000通/日の有償メッセージを消費。
・LINEの制限15,000通/月を超えると5円/通がかかることになる。
・それが仮に1ヶ月続いた。
とすると、[{(20,000通/日)　× 30日} - 15,000通] ×　(5円/通) = 2,925,000円　もの馬鹿げた使用料が発生することに！！アホか笑

　流石に⑤のような攻撃を受けることはないでしょうが、自衛はしないといけないですね。絶対にフリープランで押し通すこと。

## 3.ソースコード
　注意事項が分かった所でコード書きます。

```javascript

// lineアクセストークン設定
const ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("ACCESS_TOKEN"); // ※1

// lineのapiサーバ設定
const replyUrl = "https://api.line.me/v2/bot/message/reply";
const pushUrl = "https://api.line.me/v2/bot/message/push";
var profileGetUrl = "https://api.line.me/v2/bot/profile/";
// urlfetchのhttpリクエストヘッダー設定
const headers = {"Content-Type":"application/json; charset=UTF-8", 'Authorization':'Bearer ' + ACCESS_TOKEN};

// push通知に使う管理者用のLINE ID設定
const ADMIN_USER_ID = PropertiesService.getScriptProperties().getProperty("ADMIN_USER_ID");

// push通知
function pushNotice(message, lineId){
  var pushMessage = {"to":lineId,"messages":message};
  var pushOption = {"method":"post","headers":headers,"payload":JSON.stringify(pushMessage)};
  return UrlFetchApp.fetch(pushUrl, pushOption);
}

// リプライが送られたときに呼び出される関数
function doPost(e) {
  var event = JSON.parse(e.postData.contents).events[0]
  var replyToken = event.replyToken;
  var eventType = event.type;
  var userId = event.source.userId;
  if(eventType === "follow"){ // ユーザーに友達追加された時に実行される処理
    profileGetUrl = profileGetUrl + userId;
    var profileGetOption = {"method":"get", "headers":headers};
    var profileGetResponse = UrlFetchApp.fetch(profileGetUrl, profileGetOption);
    var displayName = JSON.parse(profileGetResponse.getContentText()).displayName;
    var pictureUrl = JSON.parse(profileGetResponse.getContentText()).pictureUrl;
    registCustomer(userId, displayName, pictureUrl); // ※2
    return;
  }else if(eventType === "message" && event.message.type === "text"){ // bot宛てに何らかのトークが送信された時に実行される処理
    var userMessage = event.message.text;
    if(userId === ADMIN_USER_ID && userMessage.lastIndexOf("」宛先ID:") !== -1){ // 管理者ユーザーかつ、push通知用のユーザーIDが正しく入力されている時に実行
      adminToUser(userMessage);
      return;
    }else{ // 定型文以外のメッセージが送られた時に実行
      userToAdmin(ADMIN_USER_ID, userMessage);
      return;
    }
  }else{
    return;
  }
  var reply = {"replyToken":replyToken, "messages":message};
  var replyOption = {"method":"post","headers":headers,"payload":JSON.stringify(reply)};
  return UrlFetchApp.fetch(replyUrl, replyOption);
}

// bot経由でADMIN_USERにpush通知する
function userToAdmin(id, message){
  for(value of values){
    if(value[1] === id){
      var displayName = value[0]; // ※3
    }
  }
  // LINEで送信するmessageを定義する
  var pushMessage = [
    {
      "type":"flex","altText":displayName + "様からトークが届きました",
      "contents":{
        "type":"carousel", // ※4
        "contents":[
          {
            "type":"bubble",
            "body":{
              "type":"box","layout":"vertical",
              "contents":[
                {"type":"text","wrap":true,"size":"sm","text":displayName + "様からトークが届きました。"},
                {"type":"text","wrap":true,"text":"「" + message + "」"},
                {"type":"text","wrap":true,"size":"sm","text":"※下のメッセージをコピーして、「」内に返信内容を入力してください"}
              ]
            }
          }
        ]
      }
    },
    {"type":"text","text":"「」宛先ID:" + id} // 送信元のLINEユーザーのIDが本文上に記述されるようにする
  ];
  pushNotice(pushMessage, id);
}

// bot経由でユーザー宛に管理者ユーザーからトークを送る関数
function adminToUser(message){
  // 本文と送り先のIDを仕分けする作業
  var idSliceNum = message.indexOf("」宛先ID:") + 6;
  var idSlice = message.slice(idSliceNum);
  var sliceStart = message.indexOf("「") + 1;
  var sliceEnd = message.lastIndexOf("」");
  var adminMessage = message.slice(sliceStart, sliceEnd);
  var pushMessage = [{"type":"text","text":adminMessage}];
  pushNotice(pushMessage, idSlice);
  return;
}
```
注釈
※1.今回スクリプトのプロパティから引っ張ってくる形にしてます。
※2.ラインの表示名がないと誰から送ってきたものなのか不明だなと思うので、LINEのプロファイルゲットをしているのですが、トークを送る度に余計なUrlFetchをするのが気持ち悪いのでスプレッドシートにユーザー情報を保存するという思想です。今回は行を減らすために省いています。
※3.シートに保存しているラインの表示名を引っ張ってきています。
※4.フレックスメッセージを使用することで通常のメッセージとの見た目変えて、少しでも使いやすくなるようにという真心。

## 4.実行結果
　実際に使用した場合の画面になります。

### 1)ユーザーからBOTにメッセージを送る
![B16E903E-3CD7-4D55-BB9F-09B8FB33ED01.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682592/9094fd55-9408-1ecb-6e22-ae78b7a67867.jpeg)

### 2)BOTから管理者ユーザーにメッセージが転送される
　カルーセルを使用して転送メッセージをひとまとめにしようとしましたが、それだとメッセージのコピーができないので、この形にしました。
![F0F49E4D-6A36-423D-8745-7E0F37EB34FB.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682592/48caaf8a-fb29-cfb7-e983-71bd60ad877e.jpeg)

### 3)管理者ユーザーからBOTに返信を送信する
　こんな感じ。
![EEF208D5-9B80-4528-9467-1276EDA921BC.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682592/b9a579e6-231b-758a-4ae0-6980cb069642.jpeg)

### 4)BOTから特定ユーザーに返信が届く
![796EA0B7-C87B-45E1-81A0-1704EB33DAF6.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682592/61d6a8c2-ee0b-8dad-8b8d-cf085aec0ff4.jpeg)


## 5.まとめ
　ここまで書いておいてなんですが、結局の所GAS×LINE BOTなんてものは、機能の制限があるため、個人の趣味のレベルでやる分には楽しめますが、小規模といえども事業として使用する場合は月額払ってちゃんとしたサポートのあるサービスを購入した方が断然良い気がしますね。

　私からは以上です。