Google Apps ScriptとSlackでカレンダーイベント通知を自動化する方法の紹介

Google CalendarとSlackは、スケジュール管理やチームコミュニケーションに欠かせないツールです。この二つを統合することで、Slackチャンネルにカレンダーイベントの通知を自動化し、全員が最新情報を把握できるようにすることができます。本記事では、Google Apps ScriptとSlackを使用してカレンダーイベント通知アプリを作成する方法を紹介します。

## 目次

2. **環境設定**
   - 必要なツールとアカウント
   - グループカレンダーIDの取得
   - Slack Webhookの作成
3. **Google Apps Scriptプロジェクトとスクリプトの作成**
   - スクリプトの記述
   - Slack通知機能の実装
   - 毎日のイベント通知
   - 新規・更新イベントの確認・投稿
   - デバッグ用ボーナス機能
4. **Apps Scriptのトリガー設定**
   - 毎日のイベント通知トリガー
   - カレンダー更新時のトリガー
5. **結論**
   - リソース

## 環境の設定
### 必要なツールとアカウント

| | 使用用途 |
|-|-|
| Googleアカウント | Google CalendarとGoogle Apps Scriptにアクセスするため。 |
| Slackワークスペース | Slackチャンネルに通知を送るため。 |

### ステップ1. グループカレンダーIDを取得する
Google Calendarと統合する前に、監視したいカレンダーのIDが必要です。

1. Google Calendarにアクセスします。
1. 設定に遷移し、「自分のカレンダーの設定」セクションに遷移します。
1. 監視したいカレンダーを選択します。
1. 「カレンダーID」を見つけてコピーします。

### ステップ2. Slack Webhookを作成する
Slackに通知を送信するには、Incoming Webhookを作成し、通知を送信する特定のSlackチャンネルにWebhook URLを取得する必要があります。

1. Slack APIにアクセス: https://api.slack.com/apps
1. 新しいアプリをスクラッチから作成します。
1. Incoming Webhooksを設定します:
   1. 左側の「Features」セクションで「Incoming Webhooks」を選択します。
   1. 「Activate Incoming Webhooks」をオンにします。
1. ワークスペースに新しいWebhookを追加します:
   1. 下にスクロールして「Add New Webhook to Workspace」をクリックします。
   1. 通知を投稿したいチャンネルを選択します。
1. 追加後、Webhook URLが表示されます。それをコピーします

:::note warn
注: `[your app name] にはインストールするボットユーザーがありません`というメッセージが表示された場合は、アプリにボットユーザーが必要であることを示しています。ボットユーザーが有効になっていることを確認してください。
:::

## Google Apps Scriptプロジェクトを作成してスクリプトを書く
これで、必要な材料（カレンダーIDとSlack Webhook URL）が揃ったので、新しいApps Scriptプロジェクトを作成する準備ができました。

### 自分のアプリの機能は3つあります:
1. Slackへの通知送信
2. 毎日のイベント投稿
3. 当日の新規または更新されたイベントの確認・送信
4. デバッグに役立つボーナス機能

### ステップ1: Google Apps Scriptプロジェクトを作成する
https://script.google.com/home にアクセスし、新しいApps Scriptプロジェクトを作成します。

### ステップ2: スクリプトを書く
スクリプトを書く前に、取得したGoogle Calendar IDとSlack Webhook URLを挿入します。

```js
const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'; // Slack Webhook URLに置き換える
const CALENDAR_ID = 'your_calendar_id@group.calendar.google.com'; // カレンダーIDに置き換える
``` 

#### Slackへの通知送信
この`sendSlackNotification`関数は、Webhook URLを使用してSlackチャンネルにメッセージを送信します。詳細はコードコメントを参照してください。

```js
// 🎯 Sending Notifications to Slack
function sendSlackNotification(message) {
  const payload = {
    text: message,
    link_names: 1, // ✅Tips: Ensures that @mentions are recognized by Slack
  };

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

  UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
}
```
:::note info
メッセージに@メンション（channel、hereなど）を追加したい場合は、ペイロードに link_names: 1 を追加します。そうしないと、メンションがテキストとして認識されます。
:::

#### 毎日のイベントの投稿
この`postDailyEvents`関数は、毎日特定の時間に当日のイベントをSlackチャンネルに投稿します。詳細はインラインコードコメントを参照してください。

<details><summary>postDailyEvents()</summary>

```js
// 🎯 Post daily events 
function postDailyEvents() {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const tomorrow = new Date(today);
  tomorrow.setDate(tomorrow.getDate() + 1);

  const events = CalendarApp.getCalendarById(CALENDAR_ID).getEvents(today, tomorrow);

  let messageHeader =  ':spiral_calendar_pad: *今日の予定を共有します:*\n\n'
  let message = messageHeader;
  let postedEvents = [];

  events.forEach(event => {
    const eventId = event.getId();
    const startTime = event.getStartTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    const endTime = event.getEndTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });

    message += `▸ *${event.getTitle()}*: \n     時間：(${startTime} ~ ${endTime})\n`;

    // Add event details to postedEvents array
    postedEvents.push({ id: eventId, startTime, endTime });
  });

  // Send a message if there are events
  if (message !== messageHeader) {
    message += `\n 本日もよろしくお願いします！`
    sendSlackNotification(message);
  } else {
    Logger.log('There are no events for today.');
  }

  // Store the posted event details to avoid duplicate posting of the same event by checkNewCalendarEvents()
  const properties = PropertiesService.getScriptProperties();
  properties.setProperty('postedEvents', JSON.stringify(postedEvents));
}
```
</details>

Slackでの結果メッセージ:

:::note info
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/97a865e7-52bc-631e-7bc1-54252b7abf41.png">
:::

#### 新規または更新されたイベントの確認・投稿
この`checkNewCalendarEvents`関数は、新規または更新されたイベントを確認し、適宜通知を送信します。具体的には、以下の二つを行います：

1. 今日のカレンダーに新しいイベントが追加された場合、Slackチャンネルにメッセージを投稿します。
2. 今日のイベントの開始/終了時間が更新された場合、Slackチャンネルにメッセージを投稿します。

詳細はインラインコードコメントを参照してください。初心者向けにコードのロジックを説明するために、多くのコードコメントが含まれていますが、ご了承ください。

<details><summary>checkNewCalendarEvents()</summary>

```js
// 1. 🎯  Post newly created events for today 
// 2. 🎯  Post today's events when their start/end time gets updated
function checkNewCalendarEvents() {
   // Access the script properties service to store and retrieve persistent key-value pairs.
  const properties = PropertiesService.getScriptProperties(); 
  // Retrieve the posted events from the previous runs or initialize an empty array if none are found.
  const postedEvents = JSON.parse(properties.getProperty('postedEvents') || '[]');  
  // Get the last checked time or use the current time if not found.
  const lastChecked = new Date(properties.getProperty('lastChecked') || new Date().getTime());  

  const now = new Date();  
  // Create a new Date object representing the start of today.
  const startOfToday = new Date(now); 
  // Set the time of the startOfToday to midnight (00:00:00). 
  startOfToday.setHours(0, 0, 0, 0);  
  // Create a new Date object representing the start of tomorrow.
  const startOfTomorrow = new Date(startOfToday);  
  // Move the date to the next day.
  startOfTomorrow.setDate(startOfTomorrow.getDate() + 1);  

  // Retrieve events from the calendar for today.
  const events = CalendarApp.getCalendarById(CALENDAR_ID).getEvents(startOfToday, startOfTomorrow); 

  // Initialize an array to store the current events.
  let newPostedEvents = [];  
  
  events.forEach(event => {  
    const eventId = event.getId();  
    // Get the start time of the event in a readable format. Remove milliseconds and only display hours and minutes
    const startTime = event.getStartTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    const endTime = event.getEndTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    // Check if the event already exists in the posted events.
    const existingEvent = postedEvents.find(e => e.id === eventId);  

    if (existingEvent) {  // If the event exists in the posted events:
      // Check if event start or end time has changed
      if (existingEvent.startTime !== startTime || existingEvent.endTime !== endTime) { 
        // Create a message indicating the event time has changed.
        const message = `@channel \n\n :pencil: *本日の予定の時間が変更されました:* \n  ▸ *${event.getTitle()}* \n        時間：(${startTime} ~ ${endTime})`;  
        sendSlackNotification(message); 
      }
    } else {  // If the event does not exist in the posted events:
    // Create a message indicating a new event has been added.
      const message = `@channel \n\n :new_1: *本日の新しい予定が追加されました:* \n  ▸ *${event.getTitle()}* \n        時間：(${startTime} ~ ${endTime})`;  
      sendSlackNotification(message);  
    }

    // Add the current event details to the new posted events array.
    newPostedEvents.push({ id: eventId, startTime, endTime });  
  });

  // Update the posted events property with the new posted events array to avoid duplicate posting of the same event by checkNewCalendarEvents() when the calendar updates
  properties.setProperty('postedEvents', JSON.stringify(newPostedEvents));  
  // Update the last checked time with the current time.
  properties.setProperty('lastChecked', now.getTime());  
}
```
</details>

Slackでの結果メッセージ:

:::note info
新規イベントの場合:
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/11650d57-d79c-7546-4111-249622e4abc7.png">

既存イベンが更新された場合:
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/b7022b88-5ab3-acd3-6cb5-345eb0b5b8f3.png">
:::

#### デバッグに役立つボーナス機能
デバッグや状態リセットのために「投稿済みイベント」プロパティを確認・クリアする関数です。

スクリプトをテストおよびデバッグするために、手動で関数を実行し、Logger.log()を使用してログを確認できます。

<details><summary>checkProperties()</summary>

```js
// 🛠️ Check the state of posted events. 
// Use it manually to debug and verify the events that have been posted.
function checkProperties() {
  const properties = PropertiesService.getScriptProperties();
  const postedEvents = JSON.parse(properties.getProperty('postedEvents'));  

  Logger.log(`Posted events: ${postedEvents}`);
}
```
</details>

<details><summary>clearChannelProperties()</summary>

```js
// 🛠️ Clears the 'postedEvents' property from script properties.
// Use it manually to reset the state if you need to start fresh, ensuring that no previous events are posted and thus avoiding potential duplication in notifications.
function clearChannelProperties() {
  const properties = PropertiesService.getScriptProperties();
  properties.deleteProperty('postedEvents');

  Logger.log('Channel properties cleared.');
}
```
</details>

### ステップ3: Apps Scriptトリガーの設定

`postDailyEvents`（毎日のイベントの投稿）と`checkNewCalendarEvents`（新規または更新されたイベントの確認・投稿）は、カレンダーを確認してSlackに通知を送信するためにトリガーを設定する必要があります。「トリガー」タブからトリガーを追加します。

ここに、私が追加した2つのトリガーのスクリーンショットがあります：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/8ff31ead-3992-c07c-e521-fbfd428c97ad.png)

#### 1. postDailyEvents関数のトリガー
この関数を毎日1回、できれば朝に実行し、その日の予定をすべて送信するために`Time-driven`トリガーを設定します。スクリーンショットのように設定します。
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/93a18174-5e69-bdd9-6146-e0a0744d1cee.png">

#### 2. checkNewCalendarEvents関数のトリガー
この関数をカレンダーが更新されるたびに実行するために、別の`From calendar`のトリガーを設定します。Apps Scriptがカレンダーの変更を監視できるように、`Calendar owner email`にCalendar ID/emailを忘れずに置き換えてください。

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/760ac2e9-fee7-00af-cad9-a48c5ed469e3.png">

## 結論
Google Apps Scriptを使用してGoogle CalendarとSlackを統合することで、自動化されたリアルタイムのイベント通知を実現できます。このセットアップはチームのコミュニケーションと効率を向上さるでしょう:rocket:。特定のニーズに合わせてスクリプトをカスタマイズてください。

## 参考資料:
- GitHubで完全なコードをチェック: https://github.com/OybekJP/CalendarNotifier
- https://developers.google.com/apps-script
- https://api.slack.com/apps
- https://api.slack.com/messaging/webhooks