GASを使ってGoogleカレンダーに特定の予定があったときに前営業日にSlackに通知を送る仕組みを作ってみる

## 概要

GASを使い、Googleカレンダーに特定の予定があった時にSlackに通知を送る仕組みを作ってみたので方法を簡単にまとめる。

## 注意

すべての予定をSlack通知したい場合、GASなんか使わずにSlackのGoogleカレンダーのアプリを使ったほうがはるかに楽

## 前提

- 下記の内容を実施しSlack通知用のSlackアプリの作成が完了していること。

https://qiita.com/miriwo/items/cbb4b37b3f5abecc835f

- GAS作成時に下記の内容を実施し、タイムゾーンが日本に設定されていること。

https://qiita.com/miriwo/items/cf21884c3f907df08e47

- 下記の方法でチェック対象のGoogleカレンダーのカレンダーIDが確認できること。

https://qiita.com/miriwo/items/b00b3282cc428fc1c480

## 方法

1. 下記のようにそれぞれのファイルでコードを記載する。
    - main.gs

        ```js
        function noticeSpecificEvent() {
          const config = getConfig();
          const today = new Date();
        
          // 日本の祝日カレンダーのID（固定）
          const holidayCalendarId = "ja.japanese#holiday@group.v.calendar.google.com";
          const holidayCalendar = CalendarApp.getCalendarById(holidayCalendarId);
        
          const targetCalender = CalendarApp.getCalendarById(config.checkTargetCalendarId);
        
          if (!getIsNoticeDay(today, holidayCalendar, config)) {
            const message = "本日は通知対象日ではないため、通知は行いません。";
            console.log(message);
            return false;
          }
        
          if (!getIsLastBusinessDayBeforeSpecificEvent(today, holidayCalendar, targetCalender, config)) {
            let message = `本日は「${config.specificEventTitle}」の予定前の最終営業日ではないため通知は行いません。`;
            console.log(message);
            return false;
          }
        
          const memberInfo = getMemberInfo();
          const slackUserIds = memberInfo.slackUserIds;
        
          let text = "";
          slackUserIds.forEach(function (slackUserId) {
            text += "<@" + slackUserId + "> ";
          });
          text +=
            "\n" +
            `本日は「${config.specificEventTitle}」の予定前の最終営業日です！\n予定を忘れないようにしましょう！`;
        
          let payload = JSON.stringify({ text: text });
        
          sendSlackMessage(payload, config);
        }
        
        function getStartOfDay(date) {
            return new Date(date.getFullYear(), date.getMonth(), date.getDate());
        }
        
        /**
         * 通知対象日かどうかのboolを返却
         *
         * @param {Date} today
         * @param {Calendar} holidayCalendar
         * @param {Object} config
         * @returns {Boolean}
         */
        function getIsNoticeDay(today, holidayCalendar, config) {
          return !getIsHolidayOrWeekend(today, holidayCalendar, config);
        }
        
        /**
         * 指定された日付が祝日か休日かどうかのboolを返却
         *
         * @param {Date} day
         * @param {Calendar} holidayCalendar
         * @param {Object} config
         * @returns {Boolean}
         */
        function getIsHolidayOrWeekend(day, holidayCalendar, config) {
          const isHoliday = holidayCalendar.getEventsForDay(day).length > 0;
          const isWeekend =
            day.getDay() === config.dayOfWeeks.saturday.id ||
            day.getDay() === config.dayOfWeeks.sunday.id;
          return isHoliday || isWeekend;
        }
        
        /**
         * 指定予定前の最終営業日かどうかのboolを返却
         *
         * @param {Date} today
         * @param {Calendar} holidayCalendar
         * @param {Calendar} targetCalender
         * @param {Object} config
         * @returns {Boolean}
         */
        function getIsLastBusinessDayBeforeSpecificEvent(today, holidayCalendar, targetCalender, config) {
          let specificEventDates = getSpecificEventDates(today, targetCalender, config);
          let isLastBusinessDayBeforeSpecificEvent = false;
        
          if (specificEventDates.length === 0) {
            const message = "指定された予定が見つかりませんでした。";
            console.log(message);
            return isLastBusinessDayBeforeSpecificEvent;
          }
        
          const subDayNumber = 1;
          let lastBusinessDayBeforeSpecificEventDates = [];
        
          specificEventDates.forEach(function (specificEventDate) {
            while (true) {
              const specificEventBeforeDate = new Date(specificEventDate);
              // 指定された予定の前日を算出
              const specificEventBeforeDay = specificEventDate.getDate() - subDayNumber;
              specificEventBeforeDate.setDate(specificEventBeforeDay);
              if (!getIsHolidayOrWeekend(specificEventBeforeDate, holidayCalendar, config)) {
                lastBusinessDayBeforeSpecificEventDates.push(specificEventBeforeDate);
                break;
              }
              // falseだった場合、特定の予定の前日を特定の予定の日としてセットして本ループを再度実行
              specificEventDate = specificEventBeforeDate;
            }
          });
        
          // 年、月、日が完全に一致するかどうかを確認
          lastBusinessDayBeforeSpecificEventDates.forEach((lastBusinessDayBeforeSpecificEventDate) => {
            if (getIsSameDate(today, lastBusinessDayBeforeSpecificEventDate)) {
              isLastBusinessDayBeforeSpecificEvent = true;
            }
          });
          return isLastBusinessDayBeforeSpecificEvent;
        }
        
        /**
         * 2つの日付が完全に一致するかどうかのboolを返却
         *
         * @param {Date} dateOne
         * @param {Date} dateTwo
         * @returns {Boolean}
         */
        function getIsSameDate(dateOne, dateTwo) {
          return dateOne.getFullYear() === dateTwo.getFullYear() &&
            dateOne.getMonth() === dateTwo.getMonth() &&
            dateOne.getDate() === dateTwo.getDate();
        }
        
        /**
         * 指定された予定の日付を取得
         *
         * @param {Date} today
         * @param {Calendar} targetCalender
         * @param {Object} config
         * @returns {Array}
         */
        function getSpecificEventDates(today, targetCalender, config) {
            const oneMonthLater = getOneMonthLater(today);
            const targetCalenderEvents = targetCalender.getEvents(today, oneMonthLater);
        
            let specificEventDates = [];
            targetCalenderEvents.forEach(function (targetCalenderEvent) {
                if (targetCalenderEvent.getTitle() === config.specificEventTitle) {
                    specificEventDates.push(targetCalenderEvent.getStartTime());
                }
            });
            return specificEventDates;
        }
        
        /**
         * 指定された日付の1ヶ月後の日付を取得
         *
         * @param {Date} date
         * @returns {Date}
         */
        function getOneMonthLater(date) {
            const newDate = new Date(date);
            newDate.setMonth(newDate.getMonth() + 1);
            return newDate;
        }
        
        /**
         * Slackにメッセージを送信
         *
         * @param {String} payload
         * @param {Object} config
         */
        function sendSlackMessage(payload, config) {
          try {
            const webhookUrl = config.webhookUrl;
          
            let options = {
              method: "post",
              contentType: "application/json",
              payload: payload,
            };
          
            UrlFetchApp.fetch(webhookUrl, options);
          } catch (e) {
            console.log(e);
            throw e;
          }
        }
        ```

    - config.gs

        ```js
        function getConfig() {
          return {
            checkTargetCalendarId: "参照するGoogleカレンダーのカレンダー ID",
            specificEventTitle: "特定の予定のタイトル",
            webhookUrl:
              "用意したSlackアプリのWebhook URL",
            dayOfWeeks: {
              sunday: {
                name: "sunday",
                id: 0,
              },
              monday: {
                name: "monday",
                id: 1,
              },
              tuesday: {
                name: "tuesday",
                id: 2,
              },
              wednesday: {
                name: "wednesday",
                id: 3,
              },
              thursday: {
                name: "thursday",
                id: 4,
              },
              friday: {
                name: "friday",
                id: 5,
              },
              saturday: {
                name: "saturday",
                id: 6,
              },
            },
          };
        }
        ```

    - memberInfo.gs

        ```js
        function getMemberInfo() {
          return {
            slackUserIds: [
              "メンションをつけたいユーザーのslackID",
            ],
          };
        }
        ```

1. main.gsのnoticeSpecificEvent()を毎日1回通知を行いたい時間に実行する設定をする

これでおそらく休日・祝日も考慮し、特定の予定の前営業日に通知が行われるはずである。

## 参考文献

https://developers.google.com/apps-script/reference/calendar/calendar?hl=ja#getEvents(Date,Date)

https://developers.google.com/apps-script/reference/calendar/calendar-event?hl=ja

https://developers.google.com/apps-script/reference/calendar/calendar-event?hl=ja#getStartTime()