Googleカレンダーの予定をLINEに通知する(GAS)

# 実現できること
Googleカレンダーに登録した予定を Google Apps Script(GAS)でLINEへ送信できる。
本記事は今日と明日の予定を送信します。

# 通知イメージ
## 予定がない場合
![スクリーンショット 2023-09-09 142751.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175117/14fcd9f0-dd72-da3d-a2d0-94301d3e1880.png)

## 予定がある場合
![スクリーンショット 2023-09-09 143803.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175117/5d1c5dad-13ff-eff5-efcf-92d47f737ec2.png)

# 準備
以下の準備が必要となります。
* LINE Notifyのアクセストークン
  * トークンは再発行が出来ませんので、必ずメモを残してください
  * メモ忘れた場合、1からトークン発行が必要です
* Googleアカウント
  * GoogleカレンダーID
  * Google Apps script

https://qiita.com/mikeneko_t98/items/76576d541caa03175670

https://qiita.com/mikeneko_t98/items/60e264941492d0b44fe5

:::note info
LINEグループでトークン設定すると複数人に対してスケジュールを共有することが出来ます。
LINEグループへ通知する場合は、**LINE Notify**をメンバーに追加する必要があります。
:::

# コード
## 事前設定
GASプロジェクトのタイムゾーンの確認をオススメします。
私はこのタイムゾーンを設定していないせいで、アメリカ時間で通知されて悩んだ箇所です。
![IMG_0575.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175117/b194879c-8691-2dbd-f6ff-75833a25550c.jpeg)

## 環境変数
コードを汎用的にしてありますので、個人情報に関わる箇所は環境変数にしています。
* LINEアクセストークン:`LINE_TOKEN`
* GoogleカレンダーID:`CALENDAR_ID`
![IMG_0576.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3175117/a610af02-f199-c578-5ae3-92098afc314a.jpeg)

## 全体像
<details><summary>コードの全体像はこちらから</summary><div>

```javascript
/**
* Googleカレンダーの
* 今日の予定をLINEに送る
*/
function sendTodaySchedule() {
  var accessToken = PropertiesService.getScriptProperties().getProperty('LINE_TOKEN');
  var message = getMessage(0);
  var options =
   {
     'method'  : 'post'
    ,'payload' : 'message=' + message
    ,'headers' : {'Authorization' : 'Bearer '+ accessToken}
    ,muteHttpExceptions:true
   };
  Logger.log(options);
  UrlFetchApp.fetch('https://notify-api.line.me/api/notify',options);

  var functin_name = arguments.callee.name;
  delTrigger(functin_name);
  setTrigger(1,7,0,functin_name);
}

/**
*  Googleカレンダーの
* 明日の予定をLINEに送る
*/
function sendTomorrowSchedule() {
  var accessToken = PropertiesService.getScriptProperties().getProperty('LINE_TOKEN');
  var message = getMessage(1);
  var options =
   {
     'method'  : 'post'
    ,'payload' : 'message=' + message
    ,'headers' : {'Authorization' : 'Bearer '+ accessToken}
    ,muteHttpExceptions:true
   };
  Logger.log(options);
  UrlFetchApp.fetch('https://notify-api.line.me/api/notify',options);

  var functin_name = arguments.callee.name;
  delTrigger(functin_name);
  setTrigger(1,18,0,functin_name);
}

/**
* メッセージ内容取得
* @param {number} 今日起算の日数
* @return {string} メッセージ内容
*/
function getMessage(prm) {
  const week = ['日','月','火','水','木','金','土'];
  var calId = PropertiesService.getScriptProperties().getProperty('CALENDAR_ID');
  var cal = CalendarApp.getCalendarById(calId);
  var date = new Date();
  var strBody = '';
  var strHeader = '\n';
  // タイトル
  if ( prm==0 ) {
    strHeader += '今日 ';
  } else if ( prm==1 ) {
    strHeader += '明日 ';
  } 
  date = new Date(date.getFullYear(),date.getMonth(),date.getDate() + prm);
  weekday = date.getDay()
  strHeader += Utilities.formatDate(date,'JST','yyyy/M/d')
                + '(' +week[weekday] + ') の予定\n';
  // 内容
  strBody = getEvents(cal,date);
  if ( _isNull(strBody) ) strBody = '予定はありません。';
  return strHeader + strBody;
}

/**
* イベント取得
* @param {object} カレンダー
* @param {date} 取得日
* @return {string} イベント情報
*/
function getEvents(prmCal,prmDate) {
  var strEvents = '';
  var strStart = '';
  var strEnd = '';
  var strTime = '';
  var strLocation = '';
  var strDescription = '';
  if ( !_isNull(prmCal) ) {
     var arrEvents = prmCal.getEventsForDay(new Date(prmDate));
     for (var i=0; i<arrEvents.length; i++) {
       if ( !_isNull(strEvents) ) strEvents += '\n';
       strStart = _HHmm(arrEvents[i].getStartTime());
       strEnd = _HHmm(arrEvents[i].getEndTime());
       if ( strStart===strEnd ) {
         strTime = '終日';
       } else {
         strTime = strStart + '～' + strEnd;
       }
       strEvents += '･' + strTime + '【' + arrEvents[i].getTitle() + '】';
       strLocation = arrEvents[i].getLocation();
       strDescription = arrEvents[i].getDescription();
       if ( !_isNull(strLocation) ) strEvents += '\n　場所：' + strLocation;
       if ( !_isNull(strDescription) ) strEvents += '\n　説明：' + strDescription;
     }
  }
  return strEvents;
}

/**
* 時間フォーマット
*/
function _HHmm(str){
  return Utilities.formatDate(str,'JST','HH:mm');
}

/**
* NULL判定
*/
function _isNull(prm) {
  if ( prm=='' || prm===null || prm===undefined ) {
    return true;
  } else {
    return 
false;
  }
}

/**
* 対象のトリガーを削除
* searchTrigger:トリガー名
*/
function delTrigger(searchTrigger) {
  const triggers = ScriptApp.getProjectTriggers();
  for(const trigger of triggers){
    if(trigger.getHandlerFunction() == searchTrigger){
      ScriptApp.deleteTrigger(trigger);
    }
  }
}

/**
* 対象のトリガーを特定の日付で作成
* setD:実行日から◯日後
* setH:時
* setM:分
* triggerName:トリガー名
*/
function setTrigger(setD,setH,setM,triggerName){
  const time = new Date();
  time.setDate(time.getDate() + setD);
  time.setHours(setH);
  time.setMinutes(setM);
  ScriptApp.newTrigger(triggerName).timeBased().at(time).create();
}
```

</div></details>

## 機能説明
ソースの説明です。

### メイン処理
以下は今日の予定通知する内容ですが、明日の予定通知も基本構造は同じです。
- `getMessage`関数
  - LINEメッセージを取得
- `delTrigger`関数
  - 同名のトリガーを一括削除
- `setTrigger`関数
  - 次回用のトリガーを作成
  - 引数は◯日後、時、分、関数名を渡していますので、◯日後、時、分はカスタマイズしてください。
    例）明日の07:00に実行したい場合は、`setTrigger(1,7,0,sendTodaySchedule)`

```javascript
function sendTodaySchedule() {
  var accessToken = PropertiesService.getScriptProperties().getProperty('LINE_TOKEN');
  var message = getMessage(0);
  var options =
   {
     'method'  : 'post'
    ,'payload' : 'message=' + message
    ,'headers' : {'Authorization' : 'Bearer '+ accessToken}
    ,muteHttpExceptions:true
   };
  Logger.log(options);
  UrlFetchApp.fetch('https://notify-api.line.me/api/notify',options);

  /**
  *トリガーの削除と作成
  */
  var functin_name = arguments.callee.name;
  delTrigger(functin_name);
  setTrigger(1,7,0,functin_name);
}
```

### トリガーの追加
通常の機能では、日次実行とすると`午前7～8時`の範囲の**どこか**で実行されるような仕様となっています。
特定の時間を指定することは可能ですが、一度だけの使い切りとなります。
cron式のように定期実行する為には、都度作成する必要があります。
```javascript
/**
* 対象のトリガーを特定の日付で作成
* setD:実行日から◯日後
* setH:時
* setM:分
* triggerName:トリガー名
*/
function setTrigger(setD,setH,setM,triggerName){
  const time = new Date();
  time.setDate(time.getDate() + setD);
  time.setHours(setH);
  time.setMinutes(setM);
  ScriptApp.newTrigger(triggerName).timeBased().at(time).create();
}
```
### トリガーの削除
前述の方法でトリガーを作成可能ですが、トリガーには上限があるため放置していると上限に達してトリガーを作成出来なくなります。
そのため、使い終わったトリガーは削除します。
```javascript
/**
* 対象のトリガーを削除
* searchTrigger:トリガー名
*/
function delTrigger(searchTrigger) {
  const triggers = ScriptApp.getProjectTriggers();
  for(const trigger of triggers){
    if(trigger.getHandlerFunction() == searchTrigger){
      ScriptApp.deleteTrigger(trigger);
    }
  }
}
```

## コードの準備後
`sendTodaySchedule`と`sendTomorrowSchedule`を手動実行してください。
通知の動作確認も兼ねていますが、翌日トリガーを作成してくれるので、動作に問題がなければ翌日から定期実行してくれるようになります。
※もし失敗したとしてもトリガー削除/作成しているのでゴミは残らないです。

# さいごに
数年寝かしていた内容なので、情報が古いかもしれませんが、どなかの役に立てれれば幸いです。