LINE BotとGasを使って 〜Googleカレンダー予定登録・変更・キャンセル編〜

LINE Botを使ってGoogleカレンダーに予定を登録、変更、キャンセルする機能を作りました。
（別の記事で<a href="https://qiita.com/iiizoo/items/c56ed0d5d901c4ff31bc">決まった時間に予定通知</a>する機能についても書いてます。）

※ LINE Botの作り方、LINE BotとGASの連携方法などは他にたくさん記事が出ているので割愛しています。

割と序盤の方で「カレンダーに直接入れる方が早いかも…」と気づいちゃったんですが、、
とりあえずやってみようということで作っています。

# LINEから予定を登録・変更・キャンセル
### 作るもの
![スクリーンショット 2021-06-28 18.37.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/578913/45e8bbbe-310f-abee-1dc7-121105affa04.png)
![スクリーンショット 2021-06-28 18.38.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/578913/e147504b-e04c-729f-ce1d-56a962205c9f.png)

###事前準備
１）LINE Botを作成する
　　生成されるチャンネルアクセストークンを使います。

２）スプレッドシートID
　　ログを出力する際に使います。

###実装方針
あまり得策ではないかもしれませんが、、
キャッシュを使い、次のメッセージに誘導したりデータを渡したりしています。
（データを渡す部分は、配列に詰めてやりたかったのですが、うまくいかず、キャッシュ使ってます）

###実装
１）最初に呼ばれる関数
　　何かしらメッセージを送信するとまずこの関数が動きます。
　　ポストされた値やキャッシュの値を見て、次の関数に渡しています。
　「わ」と入力すると一番最初のメッセージが投下される仕組み

```.js
function doPost(e) 
{ 
  try {
    let contentsData = JSON.parse(e.postData.contents).events[0];
    let replyToken = contentsData.replyToken;
    let lineType = contentsData.type; 
    if(typeof replyToken === 'undefined' || lineType === 'follow') {
      return;
    }
    let userMessage = contentsData.message.text;
    let cache = CacheService.getScriptCache();
    let cacheType = cache.get("type");
      
    if(userMessage === "わ"){ sendFirstMessage(); }
    (cacheType === null)? checkNumeric(userMessage) : execForSelected(cacheType, userMessage);
    
    return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
  } catch(e) {
    cache.remove("type");
    cache.remove("data");
  }

//~~略~~

```

２）LINE Botにポストする関数

```.js
const CHANNEL_ACCESS_TOKEN = "{LINE Botから取得したチャンネルアクセストークン}";

//--略--
  function postToLineBot(message)
  {
    let 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,
        }],
      }),
    }); 
  }
//--略--
```

３）選択に応じて回答を送信

```.js
//--略--

  //最初のメッセージ
  function sendFirstMessage() 
  { 
    postToLineBot(
      "My Scheduleへようこそ。\n番号で選択してください。\n１：登録\n２：変更/キャンセル/確認"
    );
    cache.remove("type");
  }

  //上のメッセージで選択された番号から次のメッセージを送る
  function sendMessageForSelected(value) 
  {
    let type = toHalfWidth(value); //半角変換
    
    switch(type) {
      case "1": //登録 
        postToLineBot(
          "以下の順番で送信してください。\n予定日\n開始時間\n終了時間\n予定名"
        );
        cache.put("type", "confirm");
        break;
      case "2": //変更、キャンセル、確認
        postToLineBot(
          "いつ？\n4桁の日付(mmdd)で入力してください。"
        );
        cache.put("type", "get");
        break;
      default:
        postToLineBot("エラーが発生しました。最初からやり直します。\n「わ」を送信してください");
        sendFirstMessage();
        break;
    }
  }

  //さらに渡ってきた回答に応じて処理を分ける
  function execForSelected(cacheType, userMessage) 
  {
    switch(cacheType) {
      case "confirm": //内容確認
        postToLineBot(
          "この内容で登録(変更)します。\nよろしいですか？（はいorいいえ）\n" + userMessage
        );
        cache.put("data", userMessage);
        cache.put("type", "register");
        break;
      case "register": //登録処理
        if(userMessage === 'いいえ') {
          sendFirstMessage();
        }
        registerEvent(); //TODO:変更処理がまだ出来ていない
        postToLineBot("登録(変更)完了しました。");
        cache.remove("data");
        cache.remove("type");
        break;
      case "ask_wants": //内容聞き出し
        if(userMessage === 'あ') {
          postToLineBot("以下の順で送信してください。\n変更したい予定番号\n予定日\n開始時間\n終了時間\n予定名");
          cache.put("type", "confirm");
        }
        if(userMessage === 'い') {
          postToLineBot("キャンセルしたい予定番号を送信してください。");
          cache.put("type", "cancel");
        }
        break;
      case "cancel": //キャンセル
        deleteEvent(userMessage);
        postToLineBot("予定をキャンセルしました");
        cache.remove("type");
        break;
      case "get": //取得
        let events = getEventList(userMessage);
        if(events.length === 0) {
          postToLineBot("登録されている予定はありません。");
        }
        let event = setEventsForDisplay(events);
        postToLineBot(event + "\n\n予定を変更したい場合は「あ」、キャンセルしたい場合は「い」を送信してください");
        cache.put("type", "ask_wants");
        break;
      default:
        postToLineBot("エラーが発生しました。最初からやり直します。\n「わ」を送信してください");
        break;
    }
  }
//--略--
```

４）Googleカレンダーへの処理
　Googleカレンダーの関数は<a href="https://developers.google.com/apps-script/reference/calendar/calendar-event">こちら</a>を参考

```.js
//--略--
  //登録
  function registerEvent() 
  {
    let messages = cache.get("data").split('\n');
    
    let startDatetime = new Date(toDatetime(messages[0], messages[1]));
    let endDatetime = new Date(toDatetime(messages[0], messages[2]));
    let title = messages[3];
    
    let calendar = CalendarApp.getCalendarById('{Googleアカウント}');
    calendar.createEvent(title, startDatetime, endDatetime);
    cache.remove("data");
  }
  
  //取得
  function getEventList(date)
  {
    let selectedDate = new Date(returnDateWithSlash(date));
    return CalendarApp.getDefaultCalendar().getEventsForDay(selectedDate);
  }

  //削除
  function deleteEvent(userMessage)
  {
    let eventNumber = toHalfWidth(userMessage);
    let arrayNumber = Number(eventNumber) - 1;
    let eventsArray = cache.get("array").split(',');
    let deleteId = eventsArray[Number(arrayNumber)];
    if(deleteId === undefined || deleteId === '') {
      return;
    }
    let event = CalendarApp.getDefaultCalendar().getEventById(deleteId);
    event.deleteEvent();
  }
//--略--
```

５）取得した予定を表示用に成形、予定idをキャッシュで渡す

```.js
//--略--
  //取得した予定を表示用に成形
  function setEventsForDisplay(events)
  {
    let eventsArray = [];
    let event = ""; 
    for (let i in events) {
      let count = Number(i) + 1;
      let id = events[i].getId();
      let title = events[i].getTitle();
      let startTime = Utilities.formatDate(events[i].getStartTime(), "JST", "HH:mm");
      let endTime = Utilities.formatDate(events[i].getEndTime(), "JST", "HH:mm");
      event += count + "件目\n予定：" + title + "\n開始時間：" + startTime + "\n終了時間：" + endTime + "\n\n";
      eventsArray.push(id); //１）予定idを配列に詰めて
    }
    cache.put("array", eventsArray); //2)配列をキャッシュで渡す（idを使って予定の変更やキャンセルを行うため） 
    return event;
  }

//--略--
```

###ログ
スプレッドシートにログを出力します

```.js
function ouputLog(text) 
{
  const id = "{スプレッドシートのID}";
  let spreadSheet = SpreadsheetApp.openById(id);  
  let sheetName = "{スプレッドシートのタブ名}";
  spreadSheet.getSheetByName(sheetName).appendRow([new Date(), text]);
}
```

###最後に
バレてしまったかもしれませんが、実は「予定変更」まだ実装できていないです…（利用頻度が多くないので後回しにしちゃってます…）
処理としては、一度予定のキャンセルを行った後に、新規登録する流れになるのではないかと予想してます。

また、自分用ということもありバリデーション周りが全然できていません。この辺も使うシーンに応じて各自実装いただけたらと思います。