🐡

脱MTG忘れ ~Google APIとSlackWebhookで自動リマインダーを構築してみた~

2024/09/27に公開

はじめに

毎朝仕事の始まりは、その日のスケジュールを確認して、MTG 開始時刻の数分前にアラームをかける作業を行っている方は多いと思います。

特別大変な作業でもないので、私も毎朝続けてきましたが、頭のどこかでアラームのかけ忘れはないか? MTGまであと何分だ?と心配事を常にぼんやり抱えている状態でした。

今回は、そんな心配事を取り除き作業に集中できる環境を作る為に、当日のスケジュール確認開始2分前のリマインドを自動化してみました。
もし、同じ様な思いをしている方は、初期設定だけちょっと大変ですが、ぜひ実施してみてください。

github で公開しているので、よければご利用ください。
https://github.com/naoki0803/NotifMTG

プログラムの全体像

このプログラムで出来ること

このプログラムは、OAuth2.0 を用いて Google Calendar API にアクセスし、登録されたミーティングの予定を取得して、以下の内容を Slack に通知します。

  1. 朝一、当日のスケジュール一覧通知
    image

  2. MTG 開始 2 分前のリマインド通知
    image

また、プログラムの実行は cron を利用して、shell script から自動的に実行しているので、作業漏れが発生しません。

このプログラムで出来ないこと(注意点)

現状では、以下に該当した場合リマインドの通知がされません。

  • PC 再起動後すべてのリマインド通知
  • cron で./script.sh を実行した時間以降に追加登録されたスケジュールのリマインド通知

利用している技術

  • GoogleCalendarAPI
  • OAuth2.0
  • SlackWebhook
  • shell script
  • Node.js
  • cron

フォルダ構成と機能説明

📁 フォルダ構成

app
├── .env
├── README.md
├── config
│   ├── credentials.json
│   └── token.json
├── eslint.config.mjs
├── logs
│   └── cron.log
├── package-lock.json
├── package.json
├── script.sh
└── src
    ├── googleAuth.js
    └── mtgNotif.js

🔐 認証機能 (googleAuth.js)

OAuth2.0 を使用して Google Calendar API にアクセスするための認証処理を実装しています。
保存された認証情報(token.json)を読み込み、必要に応じて credentials.json の値を利用して、新しい認証情報を取得し token.json として保存します。

https://github.com/googleworkspace/node-samples/blob/main/calendar/quickstart/index.js

config/googleAuth.js
const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const { authenticate } = require('@google-cloud/local-auth');
const { google } = require('googleapis');

// スコープの設定
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
// Pathの設定
const TOKEN_PATH = path.join(process.cwd(), 'config', 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'config', 'credentials.json');

/**
* 保存された認証情報を読み込む関数
*
* @return {Promise<OAuth2Client|null>}
*/
async function loadSavedCredentialsIfExist() {
  try {
    // トークンファイルを読み込む
    const content = await fs.readFile(TOKEN_PATH);
    // 読み込んだ内容をJSONとして解析
    const credentials = JSON.parse(content);
    // Googleの認証オブジェクトを生成
    const res = google.auth.fromJSON(credentials);
    return res; // 認証オブジェクトを返す
  } catch {
    return null; // エラーが発生した場合はnullを返す
  }
}

/**
* 認証情報を保存する関数
*
* @param {OAuth2Client} client
* @return {Promise<void>}
*/
async function saveCredentials(client) {
  // credentials.jsonを読み込み
  const content = await fs.readFile(CREDENTIALS_PATH);
  // 読み込んだ内容をJSONとして解析
  const keys = JSON.parse(content);
  // 使用する認証情報を選択(インストールされたアプリの情報か、ウェブアプリの情報か)
  const key = keys.installed || keys.web;
  // 保存するためのペイロードを作成
  const payload = JSON.stringify({
    type: 'authorized_user',
    /* eslint-disable camelcase */
    client_id: key.client_id,
    client_secret: key.client_secret,
    refresh_token: client.credentials.refresh_token,
    /* eslint-enable camelcase */
  });
  // ペイロードをトークンファイルに保存
  await fs.writeFile(TOKEN_PATH, payload);
}

/**
* APIを呼び出すための認証を行う関数
*
* @return {Promise<OAuth2Client>}
*/
async function authorize() {
  let client = await loadSavedCredentialsIfExist();
  if (client) {
    return client;
  }
  client = await authenticate({
    scopes: SCOPES,
    keyfilePath: CREDENTIALS_PATH,
  });
  if (client.credentials) {
    await saveCredentials(client);
  }
  return client;
}

// authorize関数とgenerateAuthUrl関数をエクスポート
module.exports = {
  authorize
};

🔔 予定取得と通知機能 (mtgNotif.js)

mtgNotif.js ファイルでは、googleAuth.js で認証された OAuth Client を利用して、Google Calendar から当日のスケジュールを取得します。
取得したスケジュールを整形し、その日の MTG 一覧を Slack へ通知します。
また、各ミーティングの開始 2 分前にも、Slack でリマインダー通知を行います。

src/mtgNotif.js

const { IncomingWebhook } = require('@slack/webhook');
const { authorize } = require('./googleAuth.js');
const { google } = require('googleapis');
const dayjs = require('dayjs');
const cron = require('node-cron');

require('dotenv').config();

// SlackのWebhook URLを環境変数から取得
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
const webhook = new IncomingWebhook(webhookUrl);


// Google Calendar APIからイベント(Schedule)を取得する関数
async function fetchTodayMtgSchedules() {
  try {
    // googleAuth.jsからexportしたauthorize関数を使ってOAuth2クライアントを取得
    const auth = await authorize();

    // Google Calendar APIのセットアップ
    const calendar = google.calendar({ version: 'v3', auth });

    // 今日の日付を設定
    const today = new Date();
    const startOfDay = new Date(today.setHours(0, 0, 0, 0)).toISOString();
    const endOfDay = new Date(today.setHours(23, 59, 59, 999)).toISOString(); // eslint-disable-line no-magic-numbers

    // Google Calendar APIからイベント(Schedule)の取得
    const response = await calendar.events.list({
      calendarId: process.env.CALENDAR_ID,
      timeMin: startOfDay,
      timeMax: endOfDay,
      singleEvents: true,
      orderBy: 'startTime',
    });

    // イベント(Schedule)の整形
    const events = response.data.items;
    const result = events.map(event => ({
      summary: event.summary,
      dateTime: new Date(event.start.dateTime).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }) // 日本時間に変換
    }));
    return result;
  } catch (error) {
    console.log(`スケジュールの取得に失敗しました: ${error}`);  // eslint-disable-line no-console
    return [];
  }
}

// Slackに通知する関数
async function sendMtgNotification(events) {
  try {
    // result(Scheduleの内容)を整形して、Slackに通知
    if (events.length) {
      const formattedEvents = events.map(event => `\t・ ${event.dateTime} - ${event.summary}`).join('\n');
      await webhook.send({
        text: `【本日のMTG予定】\n${formattedEvents}`
      });

      // Reminderの設定
      scheduleReminder(events);

    } else {
      await webhook.send({
        text: '【本日のMTG予定】\n 本日MTGの予定はありません'
      });
    }
  } catch (error) {
    await webhook.send({
      text: `通知送信中にエラーが発生しました: ${error}`
    });
  }
}

// 指定された会議のn分前に通知する関数
function scheduleReminder(events) {
  events.forEach(event => {
    const today = dayjs().format('YYYY-MM-DD'); // 今日の日付を取得
    const eventDateTime = `${today} ${event.dateTime}`; // 今日の日付に時間を結合
    const eventTime = dayjs(eventDateTime, 'YYYY-MM-DD HH:mm'); // 結果をdayjsオブジェクトに変換
    const reminderMinutes = 2;  // 会議の何分前に通知するかを変数に格納
    const notifyTime = eventTime.subtract(reminderMinutes, 'minute'); // 通知する時間を計算
    const cronTime = `${notifyTime.minute()} ${notifyTime.hour()} * * *`; // cron時間を設定

    cron.schedule(cronTime, () => {
      // 非同期即時実行関数(IIFE)を使って、非同期処理をその場で実行
      (async function () {
        try {
          // 非同期処理を行う(例: Slackへのメッセージ送信)
          await webhook.send({
            text: `"${event.summary}" が2分後に始まります`
          });
        } catch {
          await webhook.send({
            text: `Reminder送信中にエラーが発生しました: "${event.summary}"`
          });
        }
      })(); // ここで関数を定義すると同時に実行している
    });
  });
};

// toDayMtgNotif関数をリファクタリング
async function toDayMtgNotif() {
  try {
    const events = await fetchTodayMtgSchedules();
    await sendMtgNotification(events);
  } catch (error) {
    await webhook.send({
      text: `toDayMtgNotifの実行に失敗しました: ${error}`
    });
  }
}

toDayMtgNotif();

🤖 自動実行機能 (script.sh)

crontab で登録した実行日時に、src/mtgNotif.js を実行する為のスクリプトです。
Slack の起動もこのスクリプトの中で実行しています。

script.sh
#!/bin/bash
# Slackが起動していない場合、Slackを起動
if ! pgrep -x "Slack" > /dev/null; then
  echo "Starting Slack..."
  open -a "Slack"  # Slackを起動
  sleep 5 # 完全に起動するまで少し待つ
fi

# プロセスを停止(前日のプロセスを終了)
pid=$(ps aux | grep 'src/mtgNotif.js' | grep -v grep | awk '{print $2}')

if [ -n "$pid" ]; then
  kill "$pid"
  echo "Stopped process: $pid"
fi

cd ~/projects/NotifMTG

# mtgNotifをバックグラウンドで実行
/opt/homebrew/bin/node ~/projects/NotifMTG/src/mtgNotif.js &

cron の設定

平日 8:45 に実行する場合の crontab のサンプル

crontab -l
45 8 * * 1-5 /bin/zsh -c 'source ~/projects/NotifMTG/script.sh' >> ~/projects/NotifMTG/logs/cron.log 2>&1

利用前の環境設定

事前に以下作業を実施する必要があります。

  1. git clone と npm install
  2. Google API の利用準備
  3. Google Calendar の ID を確認
  4. SlackWebhookURL の取得
  5. cron の設定

1. git clone と npm install

Terminal で任意のディレクトリに移動して以下コマンドを一行ずつ実行

git clone https://github.com/naoki0803/NotifMTG.git
npm install

2. Google API の利用準備

https://zenn.dev/yusuke49/articles/6c147bd6308912
こちらの記事の内容をすべて実施いただき、最終的に JSON ファイルをダウンロードします。
ダウンロードした json ファイルの名前を、credentials.jsonに変更して、config フォルダ内に保存します。

1. OauthClient のスコープ

記事内の3. OAuth同意を構成の手順の手順 5 の画面で、スコープを選択しますが、
今回のプログラムの場合auth/calendar.readonlyだけ選択いただければ動きます。
image

2. 承認済みリダイレクト URI の記述

記事内の4. アクセス認証情報を作成の手順 3 の画面にある
承認済みのリダイレクト URI にhttp://localhost:3000/callbackと入力して作成をクリックしてください。
image

3. JSON ファイルのダウンロードと保存

上記画像の作成を押すと json のダウンロードが出来ますので、ファイルの名前を、credentials.jsonに変更して、config フォルダ内に保存します。

app
├── .env
├── config
│   └── credentials.json  #ここに保存します
└── src
    ├── googleAuth.js
    └── mtgNotif.js

image

3. Google Calendar の ID 確認

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

  1. MTG のスケジュールなどを入力している Google Calendar の ID を確認して、.envファイルに入力します。
.env
CALENDAR_ID=カレンダーIDを入力する
app
├── .env
├── config
│   └── credentials.json
└── src
    ├── googleAuth.js
    └── mtgNotif.js

4. SlackWebhookURL の取得

  1. 以下 URL に接続してGo to Your Appsをクリック
    https://api.slack.com/quickstart
    image

  2. Create New Appをクリック
    image

  3. 下段のFrom scratchをクリック
    image

  4. 任意のAppNameの入力と、通知を送信する Slack の Workspace を選択して Create App をクリック
    image

  5. 作成された App のサイドバーからIncoming Webhooksを選択して、Activate Incoming Webhooks をOnに変更する。
    image

  6. 同じ画面下部にあるAdd New Webhook to Workspaceをクリック
    image

  7. 通知を送信するチャンネルを選択して、許可をクリック
    image

  8. 作成された Webhook URL を.envファイルに入力します。
    image

.env
SLACK_WEBHOOK_URL=Webhook URLを入力する
app
├── .env
├── config
│   └── credentials.json
└── src
    ├── googleAuth.js
    └── mtgNotif.js

5. cron の設定

1.Terminal でcrontab -eを実行して以下 cron を登録

45 8 * * 1-5 /bin/zsh -c 'source ~<ご自身のprojectPath>/NotifMTG/script.sh' >> ~/<ご自身のprojectPath>/NotifMTG/logs/cron.log 2>&1

6. 初回起動と認証

  1. ターミナルでnode src/mtgNotif.jsを実行します。

  2. ブラウザが開き、Google アカウントのログイン画面が表示されますので、ログインして、アプリのアクセスを許可してください。

  3. 認証が成功したら、ターミナルにAuthorization completeと表示され、Slack に本日の MTG の通知が来ます。

最後に

実際に利用してみて、私にはもうなくてはならない物になっています。
朝一に実施していた手作業のアラーム設定作業と、どこかでスケジュールのことを気にかけて、ぼんやりと頭のリソースを使っている状態から開放され、作業に集中する事ができるようになりました。

初期設定は色々と大変ですが、やってみる価値はあると思いますのでぜひ。
再起動後の自動実行や、追加登録されたスケジュールの通知も順次対応しようと思います.

また、内容に不備があれば、忌憚のないご意見をお願いします。

GitHubで編集を提案

Discussion