複数Googleアカウントのカレンダーを一元管理する方法【AIアシスタント連携】

はじめに

仕事用とプライベート用で複数のGoogleアカウントを使い分けている人は多いと思います。しかし、カレンダーを複数開いて確認するのは面倒ですよね。

そこで今回、複数のGoogleカレンダーを自動監視して、新しい予定をリアルタイムで通知するツールを開発しました。個人用AIアシスタントと連携して、Telegramに自動通知します。

開発の背景

私は以下の2つのGoogleアカウントを使い分けています:

  1. 仕事用アカウント
  2. 会社の会議・MTG
  3. 顧客との打ち合わせ

  4. 副業用アカウント

  5. 体験会・研修
  6. リーダーMTG

これらを常に2つのカレンダーを開いて確認するのが非効率だったため、一元管理できるシステムを構築しました。

システム構成

アーキテクチャ

Google Calendar API
    ↓
Python監視スクリプト(30分ごとに実行)
    ↓
AIアシスタント
    ↓
Telegram通知

技術スタック

  • Python 3.11
  • Google Calendar API v3
  • OAuth 2.0認証
  • Cron – 定期実行

実装のポイント

1. Google Calendar API の認証

Google Calendar APIを使うには、OAuth 2.0認証が必要です。

credentials.json の取得

  1. Google Cloud Console{:target=”_blank” rel=”noopener noreferrer”}にアクセス
  2. プロジェクトを作成
  3. 「APIとサービス」→「OAuth同意画面」で設定
  4. 「認証情報」→「OAuth 2.0 クライアントID」を作成
  5. credentials.jsonをダウンロード

認証フロー

from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build

SCOPES = [
    'https://www.googleapis.com/auth/calendar.readonly',  # カレンダー読み取り
    'https://www.googleapis.com/auth/calendar.events'     # イベント作成・編集
]

def authenticate(account: str):
    """
    Googleアカウントを認証してトークンを保存

    Args:
        account: 'work' or 'side_job'
    """
    token_path = f'token_{account}.json'
    creds = None

    # 既存のトークンを読み込み
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)

    # トークンが無効または存在しない場合、再認証
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            # トークンをリフレッシュ
            creds.refresh(Request())
        else:
            # 新規認証(ブラウザが開く)
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)

        # トークンを保存
        with open(token_path, 'w') as token:
            token.write(creds.to_json())

    return build('calendar', 'v3', credentials=creds)

ポイント
– 各アカウントごとに別々のトークンファイル(token_work.jsontoken_side_job.json)を保存
– トークンの有効期限が切れたら自動でリフレッシュ

2. カレンダーイベントの監視

30分ごとに新しいイベントをチェックして、通知します。

import json
from datetime import datetime, timedelta

STATE_FILE = 'calendar_monitor_state.json'

def load_state():
    """前回のチェック状態を読み込む"""
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE, 'r') as f:
            return json.load(f)

    return {
        'last_check': None,
        'seen_events': {}  # {account: [event_id, ...]}
    }

def check_new_events(account: str, state: dict):
    """
    新しいイベントをチェック

    Returns:
        新しいイベントのリスト
    """
    # カレンダーサービスを取得
    service = authenticate(account)

    # 前回チェック時刻以降のイベントを取得
    last_check = state.get('last_check')
    if last_check:
        time_min = datetime.fromisoformat(last_check).isoformat() + 'Z'
    else:
        # 初回は現在時刻から
        time_min = datetime.utcnow().isoformat() + 'Z'

    time_max = (datetime.utcnow() + timedelta(days=30)).isoformat() + 'Z'

    # イベント取得
    events_result = service.events().list(
        calendarId='primary',
        timeMin=time_min,
        timeMax=time_max,
        singleEvents=True,
        orderBy='startTime'
    ).execute()

    events = events_result.get('items', [])

    # 既に見たイベントを除外
    seen_ids = set(state.get('seen_events', {}).get(account, []))
    new_events = [
        event for event in events
        if event['id'] not in seen_ids
    ]

    return new_events

def monitor_calendars():
    """全カレンダーを監視"""
    state = load_state()

    accounts = ['work', 'side_job']
    all_new_events = []

    for account in accounts:
        print(f"Checking {account}...")

        try:
            new_events = check_new_events(account, state)

            if new_events:
                print(f"  {len(new_events)} new event(s)")

                # 見たイベントIDを記録
                if account not in state['seen_events']:
                    state['seen_events'][account] = []

                for event in new_events:
                    state['seen_events'][account].append(event['id'])
                    all_new_events.append({
                        'account': account,
                        'event': event
                    })
            else:
                print(f"  No new events")

        except Exception as e:
            print(f"  Error: {e}")

    # 状態を更新
    state['last_check'] = datetime.utcnow().isoformat()
    save_state(state)

    # 通知
    if all_new_events:
        notify_new_events(all_new_events)
    else:
        print("\nNo new events to notify")

def save_state(state: dict):
    """状態を保存"""
    with open(STATE_FILE, 'w') as f:
        json.dump(state, f, indent=2)

ポイント
– 前回チェック時刻を保存して、重複通知を防ぐ
– 既に見たイベントIDを記録(seen_events
– 複数アカウントを順次チェック

3. Telegram通知の実装

Telegram Bot APIを使って通知を送信します。

def notify_new_events(events: list):
    """
    新しいイベントをTelegramに通知

    Args:
        events: [{account: str, event: dict}, ...]
    """
    message = "新しいカレンダーイベントが追加されました!\n\n"

    for item in events:
        account = item['account']
        event = item['event']

        # 日本語のアカウント名
        account_name = {
            'work': '仕事用',
            'side_job': '副業用'
        }.get(account, account)

        # イベント情報を整形
        summary = event.get('summary', '(タイトルなし)')
        start = event['start'].get('dateTime', event['start'].get('date'))

        # 日時をパース
        if 'T' in start:
            # 時刻あり
            dt = datetime.fromisoformat(start.replace('Z', '+00:00'))
            time_str = dt.strftime('%m/%d %H:%M')
        else:
            # 終日イベント
            dt = datetime.fromisoformat(start)
            time_str = dt.strftime('%m/%d')

        message += f"  {time_str} - {summary}\n"
        message += f"    ({account_name})\n\n"

    # Telegram Bot APIで送信
    import requests

    bot_token = os.getenv('TELEGRAM_BOT_TOKEN')
    chat_id = os.getenv('TELEGRAM_CHAT_ID')

    response = requests.post(
        f'https://api.telegram.org/bot{bot_token}/sendMessage',
        json={
            'chat_id': chat_id,
            'text': message,
            'parse_mode': 'Markdown'
        }
    )

    if response.status_code == 200:
        print("Notification sent to Telegram")
    else:
        print(f"Failed to notify: {response.status_code}")

4. Cronによる定期実行

30分ごとに監視スクリプトを実行:

#!/bin/bash
# run_monitor.sh

cd "$(dirname "$0")"
python3 calendar_monitor.py
# crontab -e で設定
*/30 * * * * /path/to/run_monitor.sh >> /path/to/logs/calendar_monitor.log 2>&1

実際の通知例

Telegramに以下のような通知が届きます:

新しいカレンダーイベントが追加されました!

  03/10 09:30 - 週次定例MTG
    (仕事用)

  03/12 14:00 - リーダーMTG
    (副業用)

  03/15 13:00 - 取引先との打ち合わせ
    (仕事用)

追加機能

1. 会議前リマインダー

30分前、1時間前に自動リマインド:

def check_upcoming_events():
    """30分以内に始まる予定をチェック"""
    now = datetime.now()
    threshold = now + timedelta(minutes=30)

    for account in ['work', 'side_job']:
        service = authenticate(account)

        events_result = service.events().list(
            calendarId='primary',
            timeMin=now.isoformat() + 'Z',
            timeMax=threshold.isoformat() + 'Z',
            singleEvents=True,
            orderBy='startTime'
        ).execute()

        events = events_result.get('items', [])

        for event in events:
            start = event['start'].get('dateTime')
            if start:
                start_dt = datetime.fromisoformat(start.replace('Z', '+00:00'))
                minutes_until = (start_dt - now).total_seconds() / 60

                if 25 <= minutes_until <= 30:
                    notify_reminder(event, int(minutes_until))

def notify_reminder(event: dict, minutes: int):
    """リマインダー通知"""
    summary = event.get('summary', '(タイトルなし)')
    message = f"まもなく予定があります!({minutes}分後)\n\n"
    message += f"{summary}"

    # Telegramに通知
    # ... (省略)

2. 予定のコンフリクト検出

同じ時間に複数の予定が入っている場合に警告:

def detect_conflicts():
    """予定の重複を検出"""
    all_events = []

    # 全アカウントのイベントを取得
    for account in ['work', 'side_job']:
        service = authenticate(account)
        events = get_events(service)
        all_events.extend([(account, e) for e in events])

    # 時間でソート
    all_events.sort(key=lambda x: x[1]['start'].get('dateTime', ''))

    conflicts = []
    for i in range(len(all_events) - 1):
        event1 = all_events[i][1]
        event2 = all_events[i + 1][1]

        end1 = event1['end'].get('dateTime')
        start2 = event2['start'].get('dateTime')

        if end1 and start2:
            end1_dt = datetime.fromisoformat(end1)
            start2_dt = datetime.fromisoformat(start2)

            if end1_dt > start2_dt:
                conflicts.append((event1, event2))

    if conflicts:
        notify_conflicts(conflicts)

セキュリティ対策

1. トークンの安全な保存

# トークンファイルのパーミッションを制限
chmod 600 token_*.json

2. 環境変数での管理

# .env ファイルを使用
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
from dotenv import load_dotenv

load_dotenv()

bot_token = os.getenv('TELEGRAM_BOT_TOKEN')
chat_id = os.getenv('TELEGRAM_CHAT_ID')

3. .gitignoreで秘密情報を除外

# Google Calendar認証
credentials.json
token_*.json

# 環境変数
.env

# 状態ファイル
calendar_monitor_state.json

トラブルシューティング

エラー: invalid_grant

原因:トークンの有効期限切れ

解決策

# トークンを削除して再認証
rm token_*.json
python server.py
# → ブラウザで認証

エラー: API has not been used

原因:Google Calendar APIが有効化されていない

解決策
1. Google Cloud Consoleで「APIとサービス」→「有効なAPIとサービス」
2. 「+ APIとサービスの有効化」をクリック
3. 「Google Calendar API」を検索して有効化

まとめ

複数Googleアカウントのカレンダー統合で得られた効果:

カレンダーを切り替える手間がゼロに
→ 全ての予定を一箇所で確認

新しい予定の見逃しが激減
→ 自動通知で即座に気づく

会議前のリマインダーで遅刻防止
→ 30分前通知で準備できる

予定の重複を事前に検出
→ ダブルブッキングを防ぐ

Google Calendar APIは非常に強力で、今回紹介した機能以外にも:
– イベントの自動作成
– 定期予定の管理
– 参加者への招待送信

など、様々なことが可能です。業務効率化の第一歩として、ぜひ試してみてください!


技術スタック: Python, Google Calendar API, OAuth 2.0, Telegram Bot API
開発期間: 約1週間
コード行数: 約1,000行
対応サービス: Google Calendar