予約システム（LINE BOT+GAS）開発メモ

#目的
ある理由で予約システムを作ってみることになったので、調べながらやってみることにした。備忘録としてここに残していくよ。
今回は（https://qiita.com/dekamintv/items/a4cb9a195f80ef4fbdfa#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB
）
こちらの記事のソースコードを使わせて頂き、実装する実験のメモです。
次回以降はオリジナルで作ったものを紹介できればと思います！
#GAS(Google Apps Script)とは？
Google Apps Script（GAS）とは、Googleが開発・提供しているプログラミング言語で、JavaScriptをベースに作成されている。
GASでは複数のGoogle提供サービス（Google Apps）を連携し、いろんなことができる。
GASが連携できるのは以下のGoogleのサービス。

 - Gmail
 - Googleドライブ
 - Googleカレンダー
 - Googleドキュメント
 - Googleスプレッドシート
 - Googleスライド
 - Googleマップ
 - Googleフォーム
 - Goole翻訳

また、Google以外にも、各Webサービスが提供しているAPI等を利用すれば、それらのツールとも連携ができる。
たとえば、チャットツール（Slack、Microsoft Teams、Chatworkなど）と連携すれば、「特定の相手からのメールをSlackで通知する」、「今日のGoogleカレンダーでの予定をTeamsで通知する」といった活用もできる。

#LINE messaging API + GAS
最低限、LINEで動くチャットボットを作るには、以下の3つが必要。

 - LINE Buisinessアカウント
 - httpsに対応したサーバー
 - ユーザーのメッセージに返信するためのプログラム
 
今回は、このGASを利用してサーバとプログラムを用意する。
GASはいくつか関数を用意すると、PaaSのようにウェブサーバとして利用することができる。
LINE Messaging APIを利用するには、ユーザからのメッセージを受け取るサーバーを用意する必要があるので、このサーバーをGASで用意してWebhook URLとして設定する。

#機能

#コード

```
var CHANNEL_ACCESS_TOKEN = "チャネルアクセストークン";
var ERROR_SHEET_ID = "スプレッドシートのID";

//日付、時刻のフォーマット設定
var dateExp = /(\d{2})\/(\d{2})\s(\d{2}):(\d{2})/;
var dayExp = /(\d+)[\/月](\d+)/;
var hourMinExp = /(\d+)[:時](\d+)*/;


function doPost(e) {
  try {
    handleMessage(e);
  } catch(error) {
    logging("ToCalendarFromLineBot");
    logging(JSON.stringify(e));
    logging(JSON.stringify(error));
    var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
    reply(replyToken, error.message);
  }
}


function logging(str) {
  var sheet = SpreadsheetApp.openById("スプレッドシートのID").getActiveSheet();
  var ts = new Date().toLocaleString("japanese", {timeZone: "Asia/Osaka"});
  sheet.appendRow([ts, str]);
}

function handleMessage(e) {
  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  var lineType = JSON.parse(e.postData.contents).events[0].type
  if (typeof replyToken === "undefined" || lineType === "follow") {
    return;
  }
  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;
  var cache = CacheService.getScriptCache();
  var type = cache.get("type");

  if (type === null) {
    if (userMessage === "予定追加") {
      cache.put("type", 1);
      reply(replyToken, "予定日を教えてください！\n「06/17, 6月17日」などの形式なら大丈夫です！");
    } else if (userMessage === "一週間の予定確認") {
      reply(replyToken, getEventss());
    } else {
      reply(replyToken, "リッチメニューの「予定追加」で予定追加を、「一週間の予定確認」で一週間の予定参照ができるので気軽に話しかけてくださいね！");
    }
  } else {
    if (userMessage === "キャンセル") {
      cache.remove("type");
      reply(replyToken, "キャンセルしました！");
      return;
    }

    switch(type) {
      case "1":
        // 予定日
        var [matched, month, day] = userMessage.match(dayExp); //dayexpの型に合う文字列を取り出す。
        cache.put("type", 2);
        cache.put("month", month);
        cache.put("day", day);
        reply(replyToken, month + "/" + day + "ですね！　次に開始時刻を教えてください。「13:00, 13時, 13:20, 13時20分」などの形式なら大丈夫です！");
        break;

      case "2":
        // 開始時刻
        var [matched, startHour, startMin] = userMessage.match(hourMinExp);
        cache.put("type", 3);
        cache.put("start_hour", startHour);
        if (startMin == null) startMin = "00";
        cache.put("start_min", startMin);
        reply(replyToken, startHour + ":" + startMin + "ですね！　次に終了時刻を教えてください。");
        break;

      case "3":
        // 終了時刻
        var [matched, endHour, endMin] = userMessage.match(hourMinExp);
        cache.put("type", 4);
        cache.put("end_hour", endHour);
        if (endMin == null) endMin = "00";
        cache.put("end_min", endMin);
        reply(replyToken, endHour + ":" + endMin + "ですね！　最後に予定名を教えてください！");
        break;

      case "4":
        // 予定名
        cache.put("type", 5);
        cache.put("title", userMessage);
        var [title, startDate, endDate] = createEventData(cache);
        reply(replyToken, toEventFormat(title, startDate, endDate) + "\nで間違いないでしょうか？ よろしければ「はい」をやり直す場合は「いいえ」をお願いいたします！");
        break;

      case "5":
        // 確認の回答がはい or いいえ
        cache.remove("type");
        if (userMessage === "はい") {
          var [title, startDate, endDate] = createEventData(cache);
          CalendarApp.getDefaultCalendar().createEvent(title, startDate, endDate);
          reply(replyToken, "追加しました！\nお疲れ様でした！");
        } else {
          reply(replyToken, "お手数ですがもう一度お願いいたします！");
        }
        break;
      default:
        reply(replyToken, "申し訳ありません。\n形式に誤りがないか確認してみて、なければ「キャンセル」で予定入力をキャンセルすることができるので、そちらを試していただけますか？");
        break;
    }
  }
}

function createEventData(cache) {
  var year = new Date().getFullYear();
  var title = cache.get("title");
  var startDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("start_hour") - 9, cache.get("start_min"));
  var endDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("end_hour") - 9, cache.get("end_min"));
  return [title, startDate, endDate];
}

function toEventFormat(title, startDate, endDate) {
  var start = Utilities.formatDate(startDate, "JST", "MM/dd HH:mm");
  var end = Utilities.formatDate(endDate, "JST", "MM/dd HH:mm");
  var str = title + ": " + start + " ~ " + end;
  return str;
}

function reply(replyToken, message) {
  var url = "https://api.line.me/v2/bot/message/reply";
  UrlFetchApp.fetch(url, {
    "headers": {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    "method": "post",
    "payload": JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": message,
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
}





function get_Calendar() {
  var arrCals=[];
  arrCals.push(CalendarApp.getCalendarById('使いたいGoogleカレンダーのアカウントのメールアドレス'));
  return arrCals;
}

//1週間の予定を取得するメインの関数
function get_Week_Schedule(){
  var arrCals = get_Calendar();
  var dateNow = new Date();
  var date = new Date();
  var strBody ='';
  var tmpBody ='';
  var strIntro = "おつかれさまです。\n1週間の予定です。\n" ;
  for (var j = 1; j < 7 ; j++ ){
    date.setDate(dateNow.getDate()+j);
    for (var i = 0 ; i < arrCals.length ; i++){
      tmpBody = tmpBody + getEvents(arrCals[i],date);
    }
    if (tmpBody){
      strBody = strBody + date.getDate() + '日\n' + tmpBody;
    }
    tmpBody = '';
  }
  return (strIntro + strBody);


}

//予定を取得する関数
function getEvents(Cals,getDate){
　var arrEvents = Cals.getEventsForDay(getDate);//カレンダーの予定取得
  var strEvents ='';
  for (var i=0; i<arrEvents.length; i++){
    var strTitle = arrEvents[i].getTitle();
    var strStart = _HHmm(arrEvents[i].getStartTime());
    var strEnd = _HHmm(arrEvents[i].getEndTime());
    if (strStart == strEnd){
      strEvents = strEvents + '終日イベント：' + strTitle +  '\n';
    }else{
      strEvents = strEvents + strStart + '～' + strEnd+ '：'  + strTitle  + '\n';
    }
  }
  return strEvents;
}

//予定確認時の関数
function getEventss() {
  var events = CalendarApp.getDefaultCalendar().getEventsForDay(new Date());
  var body;

  if (events.length === 0) {
    body = "今週の予定はありません!";
    return body;
  } else {
    body = get_Week_Schedule();
    return body;
  }

}

function toHHmm(date){
  return Utilities.formatDate(date, "JST", "HH:mm");
}

function _HHmm(str){
  return Utilities.formatDate(str,'JST','HH:mm');
}

```
#解説

```
function doPost(e) {
  try {
    handleMessage(e);
  } catch(error) {
    logging("ToCalendarFromLineBot");
    logging(JSON.stringify(e));
    logging(JSON.stringify(error));
    var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
    reply(replyToken, error.message);
  }
}
```
###doPost関数

WebアプリにPOSTリクエストが送られた時に実行される関数。
Postが発生した際に、POSTデータをeに格納する。

###try...catch

```
try {
  try_statements
}
catch (exception_var) {
  catch_statements
}
finally {
  finally_statements
}
```
・try_statements
実行される文です。
・catch_statements
try ブロックの中で例外が発生した場合に実行される文です。
・exception_var
関連する catch 節に対して例外オブジェクトを保持する識別子です。
・finally_statements
try 文が完了した後に実行される文です。これらの文は、例外が発生されたり捕捉されたりしたかどうかに関係なく実行されます。

#工夫したところ

 - new Date()に時間を入れると、日本時間に直そうと+9時間してくるので、createEventData関数を
 
```
 var startDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("start_hour")-9, cache.get("start_min"));
 var endDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("end_hour")-9, cache.get("end_min"));
```
このように、cache.get("start_hour")を-9時間している。
これが正しい直し方かは分からない。
ちな、cache.get("month") - 1は、月は0~11で1~12月を表しているから -1してる。

#改善点

#参考記事
・LINE Developers設定などに関して
https://udemy.benesse.co.jp/development/app/line-bot-making.html

・参考にしたソースコード
https://qiita.com/dekamintv/items/a4cb9a195f80ef4fbdfa#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB

・オウム返し
https://qiita.com/tetrapod117/items/e7b48485c98f6b88f311

・リッチメニュー作成
https://qiita.com/bow_arrow/items/32ac5d2b4c67bd0c1dc2#%E7%9B%AE%E7%9A%84

・GASについて
https://devpixiv.hatenablog.com/entry/2016/11/14/150000

・JavaScript日本時間について
https://www.jisakeisan.com/timezone/utc/
https://neos21.net/blog/2020/12/09-01.html