めんどくさがりにおススメ！LINE Botで専属の秘書を作ってみた　#Googleカレンダー #GAS

#秘書が欲しい
予定管理用にスマホにカレンダーアプリを入れていますが、めんどくさくて使いこなせていません。
スマホには色々なアプリが入っているため、もはや探すのもめんどくさい。

予定入力や確認のために、カレンダーアプリを開く事すらめんどくさい私に、毎日使うLINEからGoogleカレンダーに予定登録が出来て、しかも今日の予定や1週間分の予定も確認できるLINE Botを作りました！
Googleカレンダーに登録するため、登録内容をパソコンからも確認ができます。

LINE Botと呼ぶと愛着も湧かないので、「秘書のじいや」としました。

予定管理はじいやにお任せです！

#秘書のじいや紹介
■予定を追加してくれる

https://twitter.com/momoiku3/status/1460493469214863364

■予定を教えてくれる

https://twitter.com/momoiku3/status/1460494651379503104

■毎朝今日の予定を教えてくれる
![Screenshot_20211116-152458.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/051bb619-60eb-3bf5-38cf-ce52580dd25a.png)


#仕様
・GoogleAppsScript
・LINEmessagingAPI
・Googleカレンダー

#プログラム

■常時実行
<details><summary>全体プログラム</summary><div>

```javascript
var CHANNEL_ACCESS_TOKEN = "LINEアクセストークン";
var ERROR_SHEET_ID = "スプレッドシートのID";

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(ERROR_SHEET_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.match(/予定追加/)) {
      cache.put("type", 1);
      reply(replyToken, "予定の追加を行います。\n 登録したい日にちを入力してください。\n入力形式は「20211201」としてください。");
    } else if (userMessage.match(/今日の予定/)) {
      reply(replyToken, getEventss());

    } else if (userMessage.match(/一週間の予定/)) {
      reply(replyToken, get_Week_Schedule());

    }else{
      reply(replyToken, "何なりとお申し付けください。");
    }
  } else {
    if (userMessage === "キャンセル") {
      cache.remove("type");
      reply(replyToken, "キャンセルします。");
      return;
    }

    switch(type) {
      case "1":
        // 予定日
        let year = userMessage.substr(0,4);
        let month = userMessage.substr(4,2);
        let day = userMessage.substr(6,2);
        cache.put("type", 2);
        cache.put("year",year);
        cache.put("month", month);
        cache.put("day", day);
        let tDate = year + "/" + month + "/" + day;
        reply(replyToken, tDate + "ですね。\n次に開始時間を教えてください。\n「0612」の形で入力してください。");
        break;

      case "2":
        // 開始時刻
        let startHour = userMessage.substr(0,2);
        let startMin =  userMessage.substr(2,2);
        cache.put("type", 3);
        cache.put("start_hour", startHour);
        cache.put("start_min", startMin);
        reply(replyToken, startHour + ":" + startMin + "ですね。\n次に終了時刻を教えてください。");
        break;

      case "3":
        // 終了時刻
        let endHour = userMessage.substr(0,2);
        let endMin =  userMessage.substr(2,2);
        cache.put("type", 4);
        cache.put("end_hour", endHour);
        cache.put("end_min", endMin);
        reply(replyToken, endHour + ":" + endMin + "ですね。\n次に予定名を教えてくください。");
        break;

      case "4":
        // 予定名
        cache.put("type", 5);
        cache.put("title", userMessage);
        var [title, startDate, endDate] = createEventData(cache);
        reply(replyToken, toEventFormat(title, startDate, endDate) + "\nで合っていますか？\n登録しても良ければ「はい」を、やり直す時は「いいえ」を入力してください。");
        break;

      case "5":
        // 確認の回答がはい or いいえ
        cache.remove("type");
        if (userMessage === "はい") {
          var [title, startDate, endDate] = createEventData(cache);
          CalendarApp.getDefaultCalendar().createEvent(title, startDate, endDate);
          reply(replyToken, "予定を追加しました。");
        } else {
          reply(replyToken, "もう一度入力してください。");
        }
        break;
      default:
        reply(replyToken, weather());
        break;
    }
  }
}

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

function toEventFormat(title, startDate, endDate) {
  var start = Utilities.formatDate(startDate, "JST", "YYYY/MM/dd HH:mm");
  var end = Utilities.formatDate(endDate, "JST", "YYYY/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=[];
  //使用したいカレンダーのGメールアドレスを入力
  arrCals.push(CalendarApp.getCalendarById('***@gmail.com'));
  return arrCals;
}

//今日の予定を取得するメインの関数
function get_TodayDoday_Schedule(){
  var arrCals = get_Calendar();
  //var dateNow = new Date();
  var date = new Date();
  var strBody ='';
  var tmpBody ='';
  var strIntro = "こちらが今日の予定です。\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_TodayDoday_Schedule();
    return body;
  }

}
//■■今日の予定はここまで■■

//■■翌日から一週間分の予定はここから
//一週間分の予定を表示
function get_Week_Schedule(){

  var startDate = new Date();
  var period = 7; // 予定を表示したい日数

  startDate.setDate(startDate.getDate() + 1);
  var schedule = getSchedule(startDate, period);
  var message = scheduleToMessage(schedule, startDate, period);

  return message;  
}

// Googleカレンダーから予定取得
function getSchedule(startDate, period){
  var calendarIDs = ['***@gmail.com']; //使用したいカレンダーのGメールアドレスを入力

  var schedule = new Array(calendarIDs.length);
  for(var i=0; i<schedule.length; i++){schedule[i] = new Array(period);}

  for(var iCalendar=0; iCalendar < calendarIDs.length; iCalendar++){
    var calendar = CalendarApp.getCalendarById(calendarIDs[iCalendar]);

    var date = new Date(startDate);
    for(var iDate=0; iDate < period; iDate++){
      schedule[iCalendar][iDate] = getDayEvents(calendar, date);
      date.setDate(date.getDate() + 1);
    }
  }

  return schedule;
}  

function getDayEvents(calendar, date){
  var dayEvents = "";
  var events = calendar.getEventsForDay(date);

  for(var iEvent = 0; iEvent < events.length; iEvent++){
    var event = events[iEvent];
    var title = event.getTitle();
    var startTime = toHHmm(event.getStartTime());
    var endTime = toHHmm(event.getEndTime());

    dayEvents = dayEvents + '・' + startTime + '-' + endTime + ' ' + title + '\n';
  }

  return dayEvents;
}

function scheduleToMessage(schedule, startDate, period){
  var now = new Date();
  var body = '\n明日からの1週間の予定です。\n' 
    + '(' +_Md(now) + ' ' +toHHmm(now) + '時点)\n'
    + '--------------------\n';

  var date = new Date(startDate);
  for(var iDay=0; iDay < period; iDay++){
    body = body + _Md(date) + '\n';

    for(var iCalendar=0; iCalendar < schedule.length; iCalendar++){
      body = body + schedule[iCalendar][iDay];
    }   

    date.setDate(date.getDate() + 1);
    body = body + '\n';
  }

  return body;
}

//■■一週間分の予定はここまで■■

// 日付指定の関数
function _Md(date){
  return Utilities.formatDate(date, 'JST', 'M/d');
}

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

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

```

</div></details>
■毎朝実行
<details><summary>全体プログラム</summary><div>

```javascript
// LINE Developersに書いてあるChannel Access Token
var access_token = CHANNEL_ACCESS_TOKEN = "アクセストークン";
// 自分のユーザーIDを指定します。LINE Developersの「Your user ID」の部分です。
var to = "ユーザーID"


//送信するメッセージ定義する関数を作成します。
function createDailyMessage() {
  //メッセージを定義する
  return push("おはようございます。\n\n"+ getEventssD() +"\n\n本日も頑張りましょう。");
}


//実際にメッセージを送信する関数を作成します。
function push(text) {
//メッセージを送信(push)する時に必要なurlでこれは、皆同じなので、修正する必要ありません。
//この関数は全て基本コピペで大丈夫です。
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  //toのところにメッセージを送信したいユーザーのIDを指定します。(toは最初の方で自分のIDを指定したので、linebotから自分に送信されることになります。)
  //textの部分は、送信されるメッセージが入ります。createMessageという関数で定義したメッセージがここに入ります。
  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'text',
        'text':text,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

//■■今日の予定を取得している部分■■
function get_CalendarD() {
  var arrCals=[];
  //使用したいカレンダーのGメールアドレスを入力
  arrCals.push(CalendarApp.getCalendarById('***@gmail.com'));
  return arrCals;
}

//今日の予定を取得するメインの関数
function get_TodayDoday_ScheduleD(){
  var arrCals = get_CalendarD();
  //var dateNow = new Date();
  var date = new Date();
  var strBody ='';
  var tmpBody ='';
  var strIntro = "こちらが今日の予定です。\n" ;
  //for (var j = 1; j < 7 ; j++ ){
    //date.setDate(dateNow.getDate()+j);
    for (var i = 0 ; i < arrCals.length ; i++){
      tmpBody = tmpBody + getEventsD(arrCals[i],date);
    }
    if (tmpBody){
      strBody = strBody + date.getDate() + '日\n' + tmpBody;
    }
    tmpBody = '';
  //}
  return (strIntro + strBody);


}

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

//予定確認時の関数（呼び出す方）
function getEventssD() {
  var events = CalendarApp.getDefaultCalendar().getEventsForDay(new Date());
  var body;

  if (events.length === 0) {
    body = "今日の予定は入っていないようですね。";
    return body;
  } else {
    body = get_TodayDoday_ScheduleD();
    return body;
  }

}

//■■今日の予定はここまで■■

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

```
</div></details>

##プログラム解説
プログラム自体は、参考記事欄に記載したものをベースに使っているため、詳細はそちらから確認してください。
こちらでは、アレンジを加えた部分を解説いたします。
＜こちらを元に作成しています。＞

https://qiita.com/dekamintv/items/a4cb9a195f80ef4fbdfa


###日付・時間入力の簡易化
元々のプログラムでは、日にちや時間の入力を「11月10日」や「11:30」といった形になっていたため、もっと入力が簡単になるよう数字だけで登録できるように変更しました。
（例：10時30分なら1030と入力）
また、来年の予定も登録できるよう西暦入力に対応させました。

＜元プログラム（日付登録部分を抜粋）＞

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

//省略

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;

```

＜変更プログラム（日付登録部分を抜粋）＞

```javascript
      case "1":
        // 予定日
//＜解説＞
//元プログラムではフォーマット設定で必要な部分のみを切り出していたが、
//数字のみ入力する形としたため、指定の桁数で数字を切り取って日付形式に
//変更する仕様とした。（時間も同様）
        let year = userMessage.substr(0,4);
        let month = userMessage.substr(4,2);
        let day = userMessage.substr(6,2);
        cache.put("type", 2);
        cache.put("year",year);
        cache.put("month", month);
        cache.put("day", day);
        let tDate = year + "/" + month + "/" + day;
        reply(replyToken, tDate + "ですね。\n次に開始時間を教えてください。\n「0612」の形で入力してください。");
        break;
```

###一週間の予定取得方法の変更
元のプログラムでは1週間の予定があれば表示としていましたが、予定の有無に関わらず表示されるように変更しました。

＜元プログラム＞
当プログラムでは、この部分を削除し変更プログラムに差し替えています。

```javascript
//予定確認時の関数
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;
  }
}

```

＜変更プログラム＞

```javascript
//■■翌日から一週間分の予定はここから

//一週間分の予定を表示
function get_Week_Schedule(){

  var startDate = new Date();
  var period = 7; // 予定を表示したい日数

  startDate.setDate(startDate.getDate() + 1);
  var schedule = getSchedule(startDate, period);
  var message = scheduleToMessage(schedule, startDate, period);

  return message;  
}

//長いため以降は全体プログラムより確認してください。

```

###文章のゆらぎ対応
LINEへ入力する言葉に多少の揺らぎが生じても対応ができるようにしました。

```javascript
//元プログラム
if (userMessage === "予定追加") 

//変更プログラム
if (userMessage.match(/予定追加/))

```

###毎朝のメッセージ配信
朝の決まった時間にメッセージが届くようにGoogleAppsScriptのトリガー実行を設定しました。
設定する関数は「createDailyMessage()」です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2122228/9bf362e0-c47c-f81c-8849-9326caf78c89.png)




#参考記事
＜アレンジ用＞

https://qiita.com/y-araki-qiita/items/26224c9ca2628d6e4d65

https://qiita.com/n_oshiumi/items/a1a02e03093825f41e01

#じいやは便利
無意識でも開いてしまうLINEからトーク感覚で予定が入力できているので、今までよりも予定入力がやりやすくなりました。
また、じいやからモーニングメッセージで今日の予定が届くので、うっかり宅配便の受け取り忘れも防げそうです。
じいやはどんなに適当にメッセージを送っても、優しく「何なりとお申し付けください」と返答してくれるので可愛いので、もっと会話パターンを追加して育成したいと思います。


#今後の発展例
・[疑似彼氏](https://qiita.com/mnana/items/3836aee1b749670804dc)要素を入れると、もっと秘書らしくなります。
・現在は予定の登録・確認のみですが、削除や変更機能も付けられると、より便利になりそうです。