赤ちゃんお世話リマインダボット【GAS】【LINEbot】

# 概要
赤ちゃんのオムツや食事などのタイミングをリマインドしてくれるLINEボットを作りました。
LINEボットの接続先はGoogleAppScriptで作りました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/c6f676b0-2fa0-5089-3692-2af73e2c926b.png)

# 目指したUX
* GoogleSheetで予定を書ける
* 赤ちゃんの誕生日を基準としたの相対日で予定を書ける
    * 赤ちゃんが生まれる前に先に予定だけ書いておけるようにするため
* 日付の区間を指定して、その範囲で定期的な予定を書ける
    * 赤ちゃんの成長に合わせて、数週間単位でスケジュールが変わっていくため
* LINEでリマインドを受けられる
* LINEで今日の予定を尋ねると、返信として今日の予定が見られる

# 実装
## 設定シート
GoogleSheetに4つのシートを用意して必要な情報を記入します。
### schedule
リマインダの送信時刻を指定するためのシートです。
シート名は`schedule`とします。

A列、B列、C列、D列で指定した日時に、E列、F列に書かれた文字をリマインドします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/11153c9e-1bf7-a641-5227-34f6a46d1200.png)

### birthday
赤ちゃんの誕生日を指定するためのシートです。
シート名は`birthday`とします。

A2セル、B2セル、C2セルにそれぞれ、西暦、月、日を書きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/925942d4-3c56-4ffb-3ce4-806a3226a90d.png)

### user_id
リマインダを送るLINEアカウントのユーザID(グループIDも可）を指定するためのシートです。
シート名は`user_id`とします。

B列にユーザIDを書きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/52878a0c-fd9d-b54a-a9e0-e2fb2b12cca0.png)

### trigger
GASのトリガーが実行されるときに参照する作業用のシートです。
シート名を`trigger`としておきます。

GASがトリガーを設定するときに下図のように自動で情報が書かれますので、シート作成時点で何かを記入する必要はありません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/fea1dba4-4a79-0dab-3ff7-aabef2b29203.png" width="600px">



# コード

```Javascript
// LINE Message API アクセストークン
let ACCESS_TOKEN = '<アクセストークン>';
// 通知URL
let PUSH = "https://api.line.me/v2/bot/message/push";
// リプライ時URL
let REPLY = "https://api.line.me/v2/bot/message/reply";

/** 
 * doPost
 * POSTリクエストのハンドリング
 */
function doPost(e) {
    var json = JSON.parse(e.postData.contents);
    reply(json);
}

/** 
 * doGet
 * GETリクエストのハンドリング
 */
function doGet(e) {
    return ContentService.createTextOutput("SUCCESS");
}

/** 
 * push
 * botからメッセージを送る
 */
function push(user_id, message) {
    // リクエストヘッダ
    var headers = {
        "Content-Type" : "application/json; charset=UTF-8",
        "Authorization" : "Bearer " + ACCESS_TOKEN
    };
    // メッセージ
    var postData = {
        "to" : user_id,
        "messages" : [
            {
                "type" : "text",
                "text" : message
            }
        ]
    };
    // POSTオプション作成
    var options = {
        "method" : "POST",
        "headers" : headers,
        "payload" : JSON.stringify(postData)
    };
    return UrlFetchApp.fetch(PUSH, options);
}

/** 
 * reply
 * ユーザからのアクションに返信する
 */
function reply(data) {
    var postMsg = data.events[0].message.text;
    var replyToken = data.events[0].replyToken;
    var replyText = "";

    if(postMsg == "おはよう") {
      replyText = "今日も一日がんばろう!!";
    } else if(postMsg == "おやすみ") {
      replyText = "お疲れ様、ゆっくり休んでね";
    }else if(postMsg == "今日の予定"){
      replyText = getTodayScheduleText();
    }else if(postMsg == "更新"){
      updateReminder();
      replyText = "更新しました";
    }


    if(replyText == ""){
        return;
    } else {
        // replyするメッセージの定義
        var postData = {
          "replyToken" : replyToken,
          "messages" : [
              {
                  "type" : "text",
                  "text" : replyText
              }
          ]
        };

        // リクエストヘッダ
        var headers = {
            "Content-Type" : "application/json; charset=UTF-8",
            "Authorization" : "Bearer " + ACCESS_TOKEN
        };

        // POSTオプション作成
        var options = {
            "method" : "POST",
            "headers" : headers,
            "payload" : JSON.stringify(postData)
        };

        return UrlFetchApp.fetch(REPLY, options);      
    }
}

function getBirthday(){
  SHEET_NAME = "birthday";
  let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  let data = sheet.getDataRange().getValues();
  let year = data[1][0];
  let month = data[1][1];
  let date = data[1][2];

  let birthday = new Date(year,Number(month)-1,date);
  return birthday;

}

function addDateTime(start, date, hour, minute){
  let newdate = new Date(start);
  newdate.setDate(newdate.getDate()+date);
  newdate.setHours(newdate.getHours()+hour);
  newdate.setMinutes(newdate.getMinutes()+minute);
  return newdate;
}

function getUserId(){
  SHEET_NAME = "user_id";
  let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  let data = sheet.getDataRange().getValues();
  let ids = []
  for(let row of data){
    ids.push(row[1]);
  }
  return ids;
}

function getFutureSchedule() {
  let SHEET_NAME = "schedule"
  let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  let data = sheet.getDataRange().getValues();
  let now = new Date().getTime();
  let birthday = getBirthday();

  let schedules = [];
  //現在時刻より未来のスケジュール時間を取得
  for(let i=1;i<data.length;i++){
    let row = data[i];
    let start_day = row[0];
    let end_day = row[1];
    let hour = row[2];
    let minute = row[3];
    let action = row[4];
    let memo = row[5];
    let scheduled = row[6].split(",");
    let done = row[7].split(",");
    for(let day=start_day; day<=end_day; day++){

      //if(scheduled.includes(String(day))) continue; //schedule済みであれば処理をスキップ
      //実行予定時刻を計算
      let schedule = addDateTime(birthday, day, hour, minute); //birthdayをハードコピー
      if(schedule.getTime() < now ) continue; //関数実行時刻nowより過去のshceduleはリストに追加しない

      schedules.push({"time":schedule,"row":(i+1),"action":action, "memo":memo});
    }

  }


  const sortedSchedules = [...schedules].sort((a, b) => a.time - b.time);
  //console.log(sortedSchedules);
  return sortedSchedules;

}

function getTimeStr(dateObj){
  let hour = ("0"+dateObj.getHours()).slice(-2);
  let minute = ("0"+dateObj.getMinutes()).slice(-2);
  return hour+":"+minute

}

function getScheduleText(schedule){
  let action = schedule.action;
  let timestr = getTimeStr(schedule.time);
  return timestr + " "+action;
}

function getTodayScheduleText(){
  let schedules = getFutureSchedule();
  let today = new Date().getDate();
  let todayschedules = []
  for(let i=0;i<schedules.length;i++){
    let schedule = schedules[i];
    let time = schedule.time;
    if(time.getDate() > today) break;
    todayschedules.push(schedule);
  }

  let messages = []
  for(let i=0;i<todayschedules.length;i++){
    let schedule = todayschedules[i];
    let text = getScheduleText(schedule);
    messages.push(text);
  }

  let message = messages.join("\n");
  return message;
}

function setSchedulesNote(schedules){
  let SHEET_NAME = "trigger"
  let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  for(let i=0;i<schedules.length;i++){
    let time = schedules[i].time;
    let action = schedules[i].action;
    let memo = schedules[i].memo;
    sheet.appendRow([time, action, memo]);
  }
}

function clearScheduleNote(){
  let SHEET_NAME = "trigger";
  let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  sheet.clearContents();
}

//現在時刻より過去のスケジュールを取得
function getScheduleNote(){
  let SHEET_NAME = "trigger"
  let sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  let data = sheet.getDataRange().getValues();
  let now = new Date().getTime();

  let schedules = []
  for(let i=0;i<data.length;i++){
    let row = data[i];
    let time = new Date(row[0]);
    let action = row[1];
    let memo = row[2];
    if(time > now) continue;
    schedules.push({"time":time, "action":action, "memo":memo});
  }
  return schedules;
}


function sendMessage(){
  let schedules = getScheduleNote();
  let user_id = getUserId();
  console.log(schedules);
  console.log(user_id);
  for(let i=0;i<schedules.length;i++){
    let schedule = schedules[i];
    let time = schedule.time;
    let action = schedule.action;
    let memo = schedule.memo;
    let message = [getTimeStr(time),action, memo].join("\n");
    for(let id of user_id){
      push(id, message);
    }
    //push(user_id[0],message);

  }
  updateReminder();
}



function updateReminder(){
  let schedules = getFutureSchedule();
  schedules = schedules.slice(0,10);
  
  cleanAllTriggers();

  for(let schedule of schedules){
    let time = schedule.time;
    //set gas trigger
    ScriptApp.newTrigger("sendMessage")
     .timeBased()
     .at(time)
     .inTimezone('Asia/Tokyo')
     .create();
  }

  clearScheduleNote();
  setSchedulesNote(schedules);
}


function cleanAllTriggers() { // 登録されているTriggersを全て消す
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
}

```

# 参考にさせていただいた情報
[Class SpreadsheetApp](https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app)
[Google Apps Scriptを使えば自動でリマインダーが送れるよって話](https://qiita.com/mr91i/items/bf6796a9a03b69054e40)
[https://monoblog.jp/archives/2349](https://monoblog.jp/archives/2349)
[JavaScript：sort()を用いた並び替え方法のサンプル](https://www.nxworld.net/js-array-sort-snippets.html)
[Google Apps Script タイムゾーンの設定 | 象と散歩](https://walking-elephant.blogspot.com/2021/01/gas.timezone.html)