GAS、はじめてみました

# GAS?
Google Apps Script (GAS)を使うことでGoogle calendar上の予定を自動でSlackに転送することが可能らしく、試してみた。

# 準備：Google Calendar
1. SlackとGoogleへのアクセスがあることが前提
2. Google calendarの設定（右上の歯車）から「カレンダーを追加」→「新しいカレンダーを作成」
3. 作成したマイカレンダーに「**テスト」などで予定を追加。この時に「\*\*」を接頭辞としてつけておく。
4. 「マイカレンダーの設定」から先ほど作成したカレンダーを選択。「カレンダーの統合」にある「カレンダーID」を確認しておく（後ほどGASスクリプト内で使います）
![calender_id.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/2b636035-1c7d-46c4-adfc-9bd06d328007.png)

# 準備：Slack
1. テスト環境ではバグが生じた時にグループ全員に迷惑かかるので「#app-test」チャンネルを作成
2. [https://api.slack.com/apps](https://api.slack.com/apps)にアクセス
3. 以下の「Create New App」をクリック
![slack-api_create-new-app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/a4d7c9a8-ee28-45a3-99dd-ef3d74334565.png)
4. 今回は「From scratch」を選択します
![slack-api_create-new-app2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/65096b41-d353-4767-8840-ec139cd1954f.png)
5. 「App Name」（今回はgoogle_calendar_notificationsとします）とこのAppを使いたいworkspaceを選択して「Create App」をクリック！
![slack-api_create-new-app3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/60109824-67c3-4c83-ac48-92cd81f1fedd.png)
6. 続いてwebhooksを取得していきます。このwebhookというのは、外部からSlackの特定のチャンネルにメッセージを送信する機能らしいです。以下の写真の①が先ほど作成したApp Nameになっていることを確認し、②「incoming Webhooks」をクリック。③の「Add New Webhook」をクリックしましょう。
![slack-api_webhooks1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/66c29d02-6233-4dc6-aa79-638af8aff58f.png)
7. すると以下の画面に移動するので、このAppを使ってメッセージを自動送信したいチャンネルを選択します。
![slack-api_webhooks2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/8eb1ac5e-4358-43ad-bccb-5dd7973ad1c5.png)
8. これでSlack側の準備は終了です。取得したWebhook URLは他の人に教えないようにしましょう（あなたのチャンネルにスパムメッセージが自動で送られるかも！？）
![slack-api_webhooks3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/45d50afa-6335-4ecb-9d60-6eb7f2e9512c.png)

# 連携：GAS
1. とうとうGASの出番です！[https://script.google.com/home](https://script.google.com/home)にアクセスして「新しいプロジェクト」を追加しましょう（今回はgoogle_calendar_to_slackという名前にしました）。
※ [ref1](https://zenn.dev/lclco/articles/github-zenn--20241108), [ref2](https://www.service.andonuts.jp/posts/slack-bot-calender)を参考にさせていただきました、ありがとうございます！
![gas1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/8aca1736-5e2c-4307-aefb-055c5c811419.png)
2. 作成したプロジェクトに入ると、以下のような画面になります。この「コード.gs」の部分が実際のスクリプトです。ここに、以下をコピペしましょう。
```javascript:gas
/**
 * SlackのWebhook URL
 * SLACK_WEBHOOK_URLの部分を先ほど作成したwebhook URLに変更してください
 */
const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/XXXXXXXXXXXXXXXXXX';
/**
 * 通知済みのイベント識別子を保存する
 * @param {Array} events - 通知済みイベント識別子の配列
 */
function setNotifiedEvents(events) {
  const userProperties = PropertiesService.getUserProperties();
  const eventsToSave = Array.isArray(events) ? events : [];
  userProperties.setProperty("notifiedEvents", JSON.stringify(eventsToSave));
}

/**
 * 通知済みのイベント識別子を取得する
 * @returns {Array} - 通知済みイベント識別子の配列
 */
function getNotifiedEvents() {
  const userProperties = PropertiesService.getUserProperties();
  const notifiedEvents = userProperties.getProperty("notifiedEvents");
  return notifiedEvents ? JSON.parse(notifiedEvents) : [];
}

/**
 * 通知済みのイベント識別子リストをリセットする（デバッグ用）
 */
function resetNotifiedEvents() {
  setNotifiedEvents([]);
  Logger.log("通知済みイベント識別子リストをリセットしました。");
}

/**
 * イベント識別子の作成（開始時刻 + タイトル）
 * @param {Event} event - カレンダーイベントオブジェクト
 * @returns {string} - イベント識別子
 */
function createEventIdentifier(event) {
  const startTime = Utilities.formatDate(event.getStartTime(), 'Asia/Tokyo', 'HH:mm');
  const title = event.getTitle().trim();
  return `${startTime} | ${title}`;
}

/**
 * メインの処理を行う関数
 * 各イベントの開始30分前に通知を行い、全て通知済みならその日は通知しない
 * calendarIDの部分をコピペしたgoogle calendar IDに変更してください
 */
function sendCalendarEventsToSlack() {
  const now = new Date();

  // 土日祝日の場合は処理を終了
  if (isHoliday(now)) {
    return;
  }

  const calendarId = "c_XXXXXXXXXXXXXXXXXXX@group.calendar.google.com";
  const calendar = CalendarApp.getCalendarById(calendarId);

  if (!calendar) {
    Logger.log("カレンダーが見つかりません。");
    return;
  }

  const notifiedEvents = getNotifiedEvents();
  Logger.log("現在の通知済みリスト: " + JSON.stringify(notifiedEvents));

  const events = calendar.getEventsForDay(now);

  if (events.length === 0) {
    Logger.log("本日の予定はありません。");
    return;
  }

  // 予定が全て通知済みの場合は通知しない
  const allEventIdentifiers = events.map(event => createEventIdentifier(event));
  const allNotified = allEventIdentifiers.every(identifier => notifiedEvents.includes(identifier));

  if (allNotified) {
    Logger.log("全ての予定が既に通知済みです。通知を行いません。");
    return;
  }

  let newNotifiedEvents = [...notifiedEvents];

  events.forEach(event => {
    const title = event.getTitle().trim();
    const startTime = event.getStartTime();
    const endTime = event.getEndTime();
    const startTimeFormatted = Utilities.formatDate(startTime, 'Asia/Tokyo', 'HH:mm');
    const endTimeFormatted = Utilities.formatDate(endTime, 'Asia/Tokyo', 'HH:mm');
    const eventIdentifier = createEventIdentifier(event);

    // 通知時間 = 開始時刻の30分前
    const notificationTime = new Date(startTime.getTime() - 30 * 60 * 1000);

    // 「**」を含むイベントのみ再通知を防ぐ
    if (title.includes("**")) {
      if (newNotifiedEvents.includes(eventIdentifier)) {
        Logger.log(`再通知防止: ${eventIdentifier}`);
        return;
      }

      if (now >= notificationTime && now < startTime) {
        const cleanedTitle = title.replace("**", "").trim();
        const message = `📅 30分以内に「${cleanedTitle}」 が始まります\n🕒 ${startTimeFormatted} - ${endTimeFormatted}`;
        sendToSlack(message);

        // 通知済みに追加
        newNotifiedEvents.push(eventIdentifier);
        Logger.log(`通知済みとして追加: ${eventIdentifier}`);
      }
    } else {
      // 「**」が含まれないイベントは常に通知
      if (now >= notificationTime && now < startTime) {
        const message = `📅 30分以内に「${title}」\n🕒 ${startTimeFormatted} - ${endTimeFormatted} が始まります`;
        sendToSlack(message);
      }
    }
  });

  setNotifiedEvents(newNotifiedEvents);
}

/**
 * メッセージをSlackに送信する関数
 * @param {string} message - Slackに送信したいメッセージ
 */
function sendToSlack(message) {
  const payload = {
    'text': message
  };

  const options = {
    'method': 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  };

  try {
    UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
    Logger.log("Slack通知が送信されました: " + message);
  } catch (error) {
    Logger.log("Slack通知エラー: " + error.message);
  }
}

/**
 * 指定された日付が休日（土日祝）かどうかを判定する関数
 * @param {Date} date - チェックしたい日付
 * @returns {boolean} - 休日の場合はtrue、平日の場合はfalse
 */
function isHoliday(date) {
  const dayOfWeek = date.getDay();
  if (dayOfWeek === 0 || dayOfWeek === 6) {
    return true;
  }

  const holidayCalendar = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com');
  const holidays = holidayCalendar.getEventsForDay(date);
  return holidays.length > 0;
}
```
3. それではテストランです。以下の写真に記載の順番で進めていき、同じような実行ログが出れば成功です！
![gas3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/2abdbff6-7fd3-4c7f-a302-a6228c761bb9.png)
4. 自動化を行います。左タブから「トリガー」を選び、「トリガーを追加」します。関数は「sendCalendarEventsToSlack」を選択し、「時間主導型」とします。今回は15分おきにトリガーを起動し、予定があるかチェックするように設定します。
![gas4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/1d7b3332-6d70-4612-961a-1ca1c6981f83.png)
5. 実行ログを確認します。左タブの「実行数」をクリックしましょう。
![gas5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/10d5ec47-2c9a-4209-9aa9-cd30a95cd9be.png)
6. Slackに通知が来ています！
![Screenshot 2025-05-15 at 9.55.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3740370/ab326444-80cf-45b7-9193-2a6cf67a923b.png)

# 最後に
今回のこだわりポイントは、通知済みのイベントをリストで保存し、それらが再送信されないようにした点です！

:::note info
**もっと良い方法や他のアプリのアイデアなどあればぜひコメントください！**
:::