【非エンジニア向け】Googleカレンダーに登録されている当日の予定を毎朝まとめてSlackに通知する方法


# はじめに

 非エンジニアでコードをまったく書けない/分からないコーポレート担当が、見よう見まねでGAS（Google Apps Script）を使いワークフローを組みました！
 
ワークフローの設定手順や途中つまづいたこと、学びなどを記録しています。
なるべく非エンジニアの方がそのまま真似しやすいように、コードをまるっと載せたり、手順を詳細めに記載しました。
また、私が分からなかったことや詰まったポイントも細かく記載しています。

# WHAT

**社員の休暇予定などを共有しているGoogleカレンダーに登録されている当日の予定を、毎朝botがまとめてSlackに通知してくれるようにした。**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/4e810d8f-8e27-6911-9604-b11319e4648a.png" width=15%>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/6e32f714-5113-06bd-9cd3-71478cef1fab.png" width=60%>
※予定が1件もない日でも空のslack通知が来るように設定しています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/f502cc6b-3461-9b24-4a64-01162dc3b055.png" width=60%>
# WHY

このカレンダーは主に社員の休暇予定が登録されていますが、誰がその日休みなのか把握が漏れやすい課題がありました。

社員が各自でカレンダーを確認しなければならない状態から、通知きっかけで受動的に把握できる状態に変えたかったことがこの活動の目的です。

さらに以前はこのカレンダーに集約するではなく、各自の休暇予定に全員を招待する形で全体共有をしていました。受動的に把握できるやり方ではありましたが、全員のカレンダーが不必要に埋まってしまう課題があり、現在の形になった経緯があります。

# HOW

## ワークフローの構成

通知したいカレンダーのGoogleアカウントでGASを作成しました。
トリガーは1日おきの時間主導、毎朝10～11時に設定しています。

当初はノーコードで組む方法を検討していましたが、GASに落ち着きました。理由は後述します。

```javascript:今設定しているコード

// 指定された日が営業日か（営業日 = 「土日でない」「祝日カレンダーに予定がない」）
// 営業日 = true
function isWorkday (targetDate) {

  // targetDate の曜日を確認、週末は休む (false)
  var rest_or_work = ["REST","mon","tue","wed","thu","fri","REST"]; // 日〜土
  if ( rest_or_work [targetDate.getDay ()] == "REST" ) {
    return false;
  }; 

  // 祝日カレンダーを確認する
  var calJpHolidayUrl = "ja.japanese#holiday@group.v.calendar.google.com";
  var calJpHoliday = CalendarApp.getCalendarById (calJpHolidayUrl);
  if (calJpHoliday.getEventsForDay (targetDate).length != 0) {
    // その日に予定がなにか入っている = 祝祭日 = 営業日じゃない (false)
    return false;
  } ;

  // 全て当てはまらなければ営業日 (True)
  return true;
}

function informCalendarToSlack() {
  // debug のために任意の日付を仕込む (year,month-1,day,hour,min,sec)
  //today = new Date (2019, 10, 4, 10, 0, 0); ※これで2019/11/4
  var today = new Date ();
  // 営業日であれば実行
  // この下にLogger.log ()入れてデバッグしたら日付判定試せた
  if (isWorkday (today) == true) {
  //SlackAPIの投稿用のトークンを設定する
  let slackToken = '【ここにSlackAPIのトークンを入力】';
  //ライブラリから導入したSlackAppを定義し、トークンを設定する
  let slackApp = SlackApp.create(slackToken);
  //Slackボットがメッセージを投稿するチャンネル(randomを設定)を定義する(個人IDを指定すればDMも可)
  let channelId = "【ここに設定したいSlackチャンネル名を入力】";
  //実行ユーザーのデフォルトであるGoogleカレンダーを取得する
  let myCalendar = CalendarApp.getDefaultCalendar();
  //Googleカレンダーの予定取得する日(今日)を設定する
  let calDate = new Date();
  //今日1日のGoogleカレンダーのイベントを取得する
  let myEvent = myCalendar.getEventsForDay(calDate);
  //Slackに通知するGoogleカレンダーの予定メッセージを作成
  let message ="【本日のアナウンスカレンダー予定です:mega:】\n";
  for(let i = 0; i < myEvent.length; i++){
    //予定の開始時刻・終了時刻の時間＋分を取得する。分は0~9の場合0を付与する
    let startH = myEvent[i].getStartTime().getHours();
    let startM = myEvent[i].getStartTime().getMinutes();
    if(startM < 10) startM = "0" + startM;
    let endH = myEvent[i].getEndTime().getHours();
    let endM = myEvent[i].getEndTime().getMinutes();
    if(endM < 10) endM = "0" + endM;   
    message += "・" + myEvent[i].getTitle() + " (" + startH +":"+  startM + "~" + endH +":"+  endM + ")\n";
  }
  //SlackAppオブジェクトのpostMessageメソッドでボット投稿を行う
  slackApp.postMessage(channelId, message);
  }
}
```

## 作成手順

### 1. SlackAppを作成する。

以下の記事の通りに進めました。

[【2021年版】GASとSlack APIでボットを作成しチャンネルにメッセージを自動投稿する方法 | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門](https://auto-worker.com/blog/?p=2904)

### 2.  GASでコードとトリガーを設定する。
 
以下の記事の通りに進めました。
 
[Googleカレンダーの予定をSlackにGASで毎日通知する(無料で簡単設定) | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門](https://auto-worker.com/blog/?p=4381)

## 困ったポイントなどの記録

### GASでの実装に至った経緯

私はコードを書けないので、最初はノーコードツールでの実現を考えていました。

Zapierを用いて、
- ①毎朝10時になったら、
- ②Googleカレンダーで当日の予定を参照し、
- ③Slackに通知する

みたいな構成でいけるのではと思っていました。

①③はできましたが、②はFind Event in Google Calendarで実現できると思いきや複数結果を参照することができませんでした。これでは当日の予定が2つ以上あると、どれか1つしか返すことができない…:pensive:

また、Googleカレンダーの予定時刻になったら、Slackに通知することはできますが、朝にまとめてではなく、予定をパラパラと送るスタイルになってしまいます。妥協案としてはアリでしたが、理想とは異なったので保留としました。

前日時点で動かしておき、一日分のアウトプットまとめをどこかにテキストとしてストックしておいて、それを翌朝に送信、とかでも実現はできそうですが、最新情報を反映できないリスクがあり、予定は直前に登録される可能性もありそうなのでこちらも保留としました。

他の方法はないか探していたところ、GASで実装されたドンピシャな内容を見つけたので、これを真似てみることにしました。

[Googleカレンダーの予定をSlackにGASで毎日通知する(無料で簡単設定) | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門](https://auto-worker.com/blog/?p=4381)

#### 余談：1つのZapierアカウントで複数のGoogleカレンダーにログインできるか

Zapierは結局使わなかったので余談ですが、できました👏

弊社環境ではGoogleアカウントを切り替えるごとに毎回二段階認証が必要です。複数のGoogleカレンダーを使うのは面倒で抵抗がありましたが、Zapierは1回コネクトしてしまえば二段階認証等不要で複数使えたので便利！

### タイムゾーンの修正が必要だった

[Googleカレンダーの予定をSlackにGASで毎日通知する(無料で簡単設定) | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門](https://auto-worker.com/blog/?p=4381)の通りに設定しテストしたら、予定の時刻がおかしなことになりました（スクショ×部分）。

[象と散歩: Google Apps Script タイムゾーンの設定](https://walking-elephant.blogspot.com/2021/01/gas.timezone.html)を参考に、タイムゾーンの設定を変更したら正しい時刻で表示されました（スクショ〇部分）。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/b56e2b33-11b8-92e1-8646-1d5d7f0feb01.png" width=60%>

###  Acceptしていない予定でも拾ってくれるか

このGoogleカレンダーはほかメンバーから招待されるだけの受動的な運用なので、通知させたい予定は基本Acceptしておらず、招待されているだけの「返答待ち」ステータスになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/0a75f7d0-b45a-f70c-66a0-725886a2fd0f.png" width=50%>

これを予定として拾ってくれるか不安でしたが、ちゃんと拾ってくれました👏

### Slack通知文のカスタマイズ

想定されている通知文から、ちょっとだけカスタムしました。

#### 「本日のGoogleカレンダー予定です」→「【本日のアナウンスカレンダー予定です📢】

絵文字は`:mega:`表記でいけた！

#### 箇条書き表記に変更

各予定の頭に「・」を追加しました。
本当はMarkdownの箇条書きにしたかったけど、分からなかったので妥協。
  
### 非営業日の通知を止める

無事に実装できたものの、土日でも律儀に空の通知を送り続けてくれていました。止めたい。

トリガーで「平日のみ」などの制御ができないらしいので、コードに非営業日判定を組み込む必要がありそうでした。
ここまでは完全コピペでいけたけど、複数のコードを組み合わせるとなるとノーコード民にはハードル高い:sob:
それでも以下の記事を参考にチャレンジしてみました。

[土日は通知されたくない！ 平日・営業日だけ動く GAS (Google Apps Script) を作る | DevelopersIO](https://dev.classmethod.jp/articles/202001-workday-only-gas/)

色々調べた結果、カイゼン方針は以下の通り設定しました。

- 土日祝の通知は止める。
- 会社独自の休日も止めることはできそうだけど、以下理由で今回はいったん見送る。
    - 発生頻度が低い。
    - 公休カレンダーの整備も併せて必要になり面倒。

```javascript:元々設定してたコード

function informCalendarToSlack() {
  //SlackAPIの投稿用のトークンを設定する
  let slackToken = '【Slackトークンを入力】';
  //ライブラリから導入したSlackAppを定義し、トークンを設定する
  let slackApp = SlackApp.create(slackToken);
  //Slackボットがメッセージを投稿するチャンネル(randomを設定)を定義する(個人IDを指定すればDMも可)
  let channelId = "【Slackチャンネル名を入力】";
  //実行ユーザーのデフォルトであるGoogleカレンダーを取得する
  let myCalendar = CalendarApp.getDefaultCalendar();
  //Googleカレンダーの予定取得する日(今日)を設定する
  let calDate = new Date();
  //今日1日のGoogleカレンダーのイベントを取得する
  let myEvent = myCalendar.getEventsForDay(calDate);
  //Slackに通知するGoogleカレンダーの予定メッセージを作成
  let message ="【本日のアナウンスカレンダー予定です:mega:】\n";
  for(let i = 0; i < myEvent.length; i++){
    //予定の開始時刻・終了時刻の時間＋分を取得する。分は0~9の場合0を付与する
    let startH = myEvent[i].getStartTime().getHours();
    let startM = myEvent[i].getStartTime().getMinutes();
    if(startM < 10) startM = "0" + startM;
    let endH = myEvent[i].getEndTime().getHours();
    let endM = myEvent[i].getEndTime().getMinutes();
    if(endM < 10) endM = "0" + endM;   
    message += "・" + myEvent[i].getTitle() + " (" + startH +":"+  startM + "~" + endH +":"+  endM + ")\n";
  }
  //SlackAppオブジェクトのpostMessageメソッドでボット投稿を行う
  slackApp.postMessage(channelId, message);
}
```

```javascript:参照サイトのコードを、main関数ごと冒頭にそのままぶちこんでみたもの（失敗）

// 指定された日が営業日か（営業日 = 「土日でない」「祝日カレンダーに予定がない」）
// 営業日 = true
function isWorkday (targetDate) {

  // targetDate の曜日を確認、週末は休む (false)
  var rest_or_work = ["REST","mon","tue","wed","thu","fri","REST"]; // 日〜土
  if ( rest_or_work [targetDate.getDay ()] == "REST" ) {
    return false;
  }; 

  // 祝日カレンダーを確認する
  var calJpHolidayUrl = "ja.japanese#holiday@group.v.calendar.google.com";
  var calJpHoliday = CalendarApp.getCalendarById (calJpHolidayUrl);
  if (calJpHoliday.getEventsForDay (targetDate).length != 0) {
    // その日に予定がなにか入っている = 祝祭日 = 営業日じゃない (false)
    return false;
  } ;

  // 全て当てはまらなければ営業日 (True)
  return true;
}

// main
function main () {

  var today = new Date (2022, 2, 11, 10, 0, 0);

  // debug のために任意の日付を仕込む (year,month-1,day,hour,min,sec)
  //today = new Date (2022, 2, 12, 10, 0, 0);

  // 営業日であれば実行
  if (isWorkday (today) == true) {
    // ....
  }
}

function informCalendarToSlack() {
  //SlackAPIの投稿用のトークンを設定する
  let slackToken = '【Slackトークンを入力】';
  //ライブラリから導入したSlackAppを定義し、トークンを設定する
  let slackApp = SlackApp.create(slackToken);
  //Slackボットがメッセージを投稿するチャンネル(randomを設定)を定義する(個人IDを指定すればDMも可)
  let channelId = "【Slackチャンネル名を入力】";
  //実行ユーザーのデフォルトであるGoogleカレンダーを取得する
  let myCalendar = CalendarApp.getDefaultCalendar();
  //Googleカレンダーの予定取得する日(今日)を設定する
  let calDate = new Date();
  //今日1日のGoogleカレンダーのイベントを取得する
  let myEvent = myCalendar.getEventsForDay(calDate);
  //Slackに通知するGoogleカレンダーの予定メッセージを作成
  let message ="【本日のアナウンスカレンダー予定です:mega:】\n";
  for(let i = 0; i < myEvent.length; i++){
    //予定の開始時刻・終了時刻の時間＋分を取得する。分は0~9の場合0を付与する
    let startH = myEvent[i].getStartTime().getHours();
    let startM = myEvent[i].getStartTime().getMinutes();
    if(startM < 10) startM = "0" + startM;
    let endH = myEvent[i].getEndTime().getHours();
    let endM = myEvent[i].getEndTime().getMinutes();
    if(endM < 10) endM = "0" + endM;   
    message += "・" + myEvent[i].getTitle() + " (" + startH +":"+  startM + "~" + endH +":"+  endM + ")\n";
  }
  //SlackAppオブジェクトのpostMessageメソッドでボット投稿を行う
  slackApp.postMessage(channelId, message);
}
```

結果は**失敗**。土日想定でも動きませんでした（＝Slack通知が飛ばなかった）が、平日想定でも動きませんでした。

```javascript:main関数なるものを消し去って、informCalendarToSlackの最初にぶちこんでみたもの（成功）

// 指定された日が営業日か（営業日 = 「土日でない」「祝日カレンダーに予定がない」）
// 営業日 = true
function isWorkday (targetDate) {

  // targetDate の曜日を確認、週末は休む (false)
  var rest_or_work = ["REST","mon","tue","wed","thu","fri","REST"]; // 日〜土
  if ( rest_or_work [targetDate.getDay ()] == "REST" ) {
    return false;
  }; 

  // 祝日カレンダーを確認する
  var calJpHolidayUrl = "ja.japanese#holiday@group.v.calendar.google.com";
  var calJpHoliday = CalendarApp.getCalendarById (calJpHolidayUrl);
  if (calJpHoliday.getEventsForDay (targetDate).length != 0) {
    // その日に予定がなにか入っている = 祝祭日 = 営業日じゃない (false)
    return false;
  } ;

  // 全て当てはまらなければ営業日 (True)
  return true;
}

function informCalendarToSlack() {
  // debug のために任意の日付を仕込む (year,month-1,day,hour,min,sec)
  //today = new Date (2019, 10, 4, 10, 0, 0); ※これで2019/11/4
  var today = new Date ();
  // 営業日であれば実行
  // この下にLogger.log ()入れてデバッグしたら日付判定試せた
  if (isWorkday (today) == true) {
  //SlackAPIの投稿用のトークンを設定する
  let slackToken = '【Slackトークンを入力】';
  //ライブラリから導入したSlackAppを定義し、トークンを設定する
  let slackApp = SlackApp.create(slackToken);
  //Slackボットがメッセージを投稿するチャンネル(randomを設定)を定義する(個人IDを指定すればDMも可)
  let channelId = "【Slackチャンネル名を入力】";
  //実行ユーザーのデフォルトであるGoogleカレンダーを取得する
  let myCalendar = CalendarApp.getDefaultCalendar();
  //Googleカレンダーの予定取得する日(今日)を設定する
  let calDate = new Date();
  //今日1日のGoogleカレンダーのイベントを取得する
  let myEvent = myCalendar.getEventsForDay(calDate);
  //Slackに通知するGoogleカレンダーの予定メッセージを作成
  let message ="【本日のアナウンスカレンダー予定です:mega:】\n";
  for(let i = 0; i < myEvent.length; i++){
    //予定の開始時刻・終了時刻の時間＋分を取得する。分は0~9の場合0を付与する
    let startH = myEvent[i].getStartTime().getHours();
    let startM = myEvent[i].getStartTime().getMinutes();
    if(startM < 10) startM = "0" + startM;
    let endH = myEvent[i].getEndTime().getHours();
    let endM = myEvent[i].getEndTime().getMinutes();
    if(endM < 10) endM = "0" + endM;   
    message += "・" + myEvent[i].getTitle() + " (" + startH +":"+  startM + "~" + endH +":"+  endM + ")\n";
  }
  //SlackAppオブジェクトのpostMessageメソッドでボット投稿を行う
  slackApp.postMessage(channelId, message);
  }
}
```
結果は**成功**。平日想定では動き、土日祝想定ではちゃんと動かなかった！

この結果にたどり着くまでに以下のような試行錯誤をしています。

#### `if (isWorkday (today) == true) {}`で、`//SlackAPIの投稿用のトークンを設定する`以下を包んでみた

ノーコード民としては、複数のコードを見よう見まねで組み合わせる場合、どの括弧が対応しているのかの解読が難しい:joy:

このif文がtrueの時にSlack通知の関数が動く（非稼働日はfalseとなって動かない）ということだと理解したので、非稼働日に動かしたくなさそうなものをこのif文で包めばいいのかなと考えました。

#### ロガーの活用

色々な日付を想定してテストしてみました。
その際に以下のように `Logger.log()` を入れて、`new Date()` にいろいろ日付を仕込んだのがちゃんと反映されるか試しました（ちゃんと仕込み通りの日付が返された！）。

```
var today = new Date ();
Logger.log ()  
if (isWorkday (today) == true) {
```
 
#### 最終テスト

最終的に`Logger.log()`を消して、色々な日付を設定したうえで実行ボタンを押して、Slackチャンネルに通知が届くか試しました。

- 2022/3/12（土）：【成功】通知飛ばず実行完了
- 2022/3/11（金）：【成功】通知飛んで実行完了
- 2022/11/3（木・祝）：【成功】通知飛ばず実行完了
- ()内空っぽ（作業日当日想定）：【成功】通知飛んで実行完了

### Slack通知が2回送信される謎（未解決）

非稼働日通知オフのためにコードを書き換えた後、数日は想定通り動いていました。
祝日と土日を挟んでも問題なく停止されていましたが、連休明けの月曜日に見守っていたら、なぜか同じSlack通知が2回送信されていました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/e8ed8d61-2036-64ce-46b1-8a0408ca6206.png" width=60%>
ログを確認し原因解明しようとしたが、不明でした。
設定日（2/8）以降、平日も土日祝もそれぞれ毎日1回ずつで正しく実行されているのに、2/14だけ突如2回実行されている。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/5a0b58b3-800e-9990-0754-90fe69e072a6.png" width=80%>
2/8以降、コードもトリガーも触っていない。詳細ログにも情報がなく不明。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530171/5a0b58b3-800e-9990-0754-90fe69e072a6.png" width=80%>
その後2/28まで、また正しく動いているのでいったん忘れることにしました。

たまに同様の事象がある模様？
- [teratail[テラテイル]: [GAS]スプレットシートのForm送信時トリガが複数回(2回)実行される](https://teratail.com/questions/222372)
- [teratail[テラテイル]: GASで複数回トリガーが発火してしまう](https://teratail.com/questions/285319)

# おわりに

このワークフローを実装してから、社員の休暇予定がより全社に浸透するようになったと感じています。実装して良かったです！

GASには苦手意識がありましたが、一定iPaaSで解決できないことがGASでは実装できたりするので、引き続き攻略していきたいと思います。