PR

ローカル環境から AWS へ!Gmail スケジュール連携ツール移行ガイド -第4段階: 抽出したスケジュール情報を Google Calendar に登録・削除する

モニタにPythonコードが表示され、AWSのロゴとGmail、Google Calendarのアイコンが配置されたイメージ。サーバーレス移行と連携処理の概念を示す。 技術・IT
AWS LambdaによるGmail連携スケジュール管理のサーバーレス化イメージ

前回の第3段階では、Gmail から取得した Garoon のメール本文を解析し、予定のタイトル・開始/終了日時・参加者・場所といった情報を抽出する処理を実装しました。

第4段階となる今回は、いよいよ抽出したスケジュール情報を Google カレンダーに登録・削除する処理 を実装していきます。件名に含まれる [登録] または [削除] のキーワードに応じて処理を切り分け、重複登録の防止も行います。

前回の記事はこちら↓

1.Google Calendar API の準備と Lambda レイヤーの設定

Google Calendar API を AWS Lambda で利用するには、google-api-python-client 等のライブラリを Lambda レイヤー として追加する必要があります。

Lambda レイヤーは、関数コードとは別に管理できる外部ライブラリのアーカイブです。

第1段階で Google Calendar API の有効化と認証情報の取得は完了している前提で進めます。まだの方は、第1段階の記事(第1段階の記事へのリンク)を参照して、必要な設定を済ませてください。

Lambda レイヤーの作成手順:

1. 必要なライブラリのインストール (ローカル環境):

まず、ローカルのコンピュータに以下のライブラリをインストールします。

pip install google-api-python-client
pip install google-auth-httplib2
pip install google-auth-oauthlib
2. Lambda レイヤーとしてパッケージング (重要なポイント):

インストールしたライブラリを Lambda レイヤーとしてアップロードするためのディレクトリ構造を作成し、ZIP ファイルに圧縮します。ここで、Python のバージョンに対応したフォルダ名は、Lambda 関数のランタイムと完全に一致させる必要があります。

Python のバージョンに合わせたディレクトリ構造を作成し、レイヤー用にパッケージします。例: Python 3.13 の場合

mkdir -p python/lib/python3.13/site-packages
cd python/lib/python3.13/site-packages
pip install --target . google-api-python-client google-auth-httplib2 google-auth-oauthlib
cd ../../../../
zip -r google_api_layer.zip python
3. Lambda レイヤーの作成:

AWS Lambda コンソールを開き、「レイヤー」を選択し、「レイヤーの作成」をクリックします。

  • 名前: google-api-client-layer など、分かりやすい名前を入力します。
  • 説明: (任意) Google API クライアントライブラリなどと記述します。
  • アップロード元: 「.zip ファイルをアップロード」を選択し、先ほど作成した google_api_layer.zip ファイルを選択します。
  • 互換性のあるアーキテクチャ: Lambda 関数のアーキテクチャに合わせて選択します(通常は arm64 または x86_64)。
  • 互換性のあるランタイム: 必ず Lambda 関数のランタイム(この場合は Python 3.13)を選択してください。
  • 「作成」をクリックします。
4. Lambda 関数へのレイヤーの追加:
  • 作成した Lambda 関数を選択し、「関数概要」の下にある「レイヤー」をクリック
  • 「レイヤーを追加」をクリックし、「AWS のレイヤー」を選択、「カスタムレイヤー」を選択
  • 先ほど作成したレイヤー (google-api-client-layer) と希望するバージョンを選択し、「追加」をクリック

Google Calendar API を操作する関数の作成 (google_calendar_connector.py)

Google Calendar API とのやり取りを main.py から分離するため、新しいファイル google_calendar_connector.py を作成し、関連する関数を記述します。

Lambda 関数のコードエディタで「新しいファイルを作成」を選択し、ファイル名に google_calendar_connector.py と入力して作成します。

以下のコードは、Google Calendar API を用いてイベントの登録、削除、重複確認を行うモジュールです。

作成した google_calendar_connector.py に、以下の Python コードを記述します。

Python

from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from googleapiclient.errors import HttpError
from dateutil import parser
from datetime import timedelta, timezone, datetime

def create_calendar_service(credentials):
    """Google Calendar API のサービスオブジェクトを生成します。"""
    if not credentials:
        print("エラー (Google Calendar API サービス作成): Credentials が提供されていません。")
        return None
    try:
        service = build('calendar', 'v3', credentials=credentials)
        print("ログ: Google Calendar API サービスオブジェクトを作成しました。")
        return service
    except Exception as e:
        print(f"エラー (Google Calendar API サービス作成): {e}")
        return None

def insert_event(service, event):
    """Google Calendar にイベントを挿入します。"""
    try:
        created_event = service.events().insert(calendarId='primary', body=event).execute()
        print(f"ログ: Google Calendar にイベントを登録しました: {created_event.get('htmlLink')}")
        return created_event
    except HttpError as error:
        print(f'エラー (Google Calendar イベント登録): {error}')
        return None

def delete_event(service, event_id):
    """Google Calendar から指定された ID のイベントを削除します。"""
    try:
        service.events().delete(calendarId='primary', eventId=event_id).execute()
        print(f"ログ: Google Calendar からイベント '{{event_id}}' を削除しました。")
        return True
    except Exception as e:
        print(f"エラー (Google Calendar イベント削除): {e}")
        return False

def get_events_by_title_and_time(service, title, start_time_iso, end_time_iso=None):
    """指定したタイトルと時間範囲に一致するイベントを検索します。重複登録を防ぐために使用します。"""
    try:
        start_datetime = datetime.fromisoformat(start_time_iso.replace('Z', '+00:00')).astimezone(timezone.utc)
        if end_time_iso:
            end_datetime = datetime.fromisoformat(end_time_iso.replace('Z', '+00:00')).astimezone(timezone.utc)
        else:
            end_datetime = start_datetime + timedelta(minutes=1)

        # 前後30秒の幅を持たせて検索
        timeMin = (start_datetime - timedelta(seconds=30)).isoformat(timespec='seconds').replace('+00:00', 'Z')
        timeMax = (end_datetime + timedelta(seconds=30)).isoformat(timespec='seconds').replace('+00:00', 'Z')
    except Exception as e:
        print(f"エラー (日付変換): {e}")
        return None

    try:
        events_result = service.events().list(
            calendarId='primary',
            timeMin=timeMin,
            timeMax=timeMax,
            singleEvents=True,
            q=title
        ).execute()

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

        for event in events:
            event_title = event.get('summary', '')
            event_start_str = event.get('start', {}).get('dateTime', '')
            event_end_str = event.get('end', {}).get('dateTime', '')

            try:
                event_start = parser.isoparse(event_start_str).astimezone(timezone.utc)
                event_end = parser.isoparse(event_end_str).astimezone(timezone.utc)
            except Exception as e:
                print(f"エラー (日付解析): {e}")
                continue

            if (event_title == title and 
                abs((event_start - start_datetime).total_seconds()) < 30 and
                abs((event_end - end_datetime).total_seconds()) < 30):
                print(f"ログ: イベントが見つかりました: {event_title} ({event_start} - {event_end})")
                return event['id']

        return None

    except HttpError as error:
        print(f'エラー (イベント検索): {error}')
        return None
    except Exception as e:
        print(f"エラー (Google Calendar イベント検索): {e}")
        return None
google_calendar_connector.py では、以下の関数を定義しています。
  • create_calendar_service(credentials) Google Calendar API を操作するためのサービスオブジェクトを作成します。引数には認証済みの Credentials オブジェクトを渡します。認証情報が不正な場合や API 生成に失敗した場合は None を返します。
  • insert_event(service, event) 指定した event(イベントの辞書オブジェクト)を Google Calendar に登録します。登録に成功すると、作成されたイベントの情報(URL など)を含むレスポンスを返します。失敗した場合は None を返します。
  • delete_event(service, event_id) イベントの一意な ID を指定して、Google Calendar から削除します。削除に成功すると True、失敗した場合は False を返します。
  • get_events_by_title_and_time(service, title, start_time_iso, end_time_iso=None) イベントのタイトルと開始・終了時刻を指定して、Google Calendar 上に既に登録されている同一イベントの存在を確認します。該当イベントが見つかった場合はその event_id を返し、見つからなければ None を返します。開始・終了時刻には ±30 秒の幅を持たせて、Garoon 側とのわずかな誤差にも対応します。

このように、google_calendar_connector.py では、Google Calendar に対する「登録」「削除」「重複チェック」の基本操作をすべてカバーしており、Lambda 関数から簡単に再利用できる構成になっています。

3. main.py を修正して Google Calendar を操作する

次に、main.py を編集し、抽出したスケジュール情報に基づいて Google Calendar を操作する処理を追加します。

main.pylambda_handler 関数内の、スケジュール情報を解析した部分に以下のコードを追加・修正します。

以下は lambda_handler 関数の主要処理です(一部省略):

認証情報の準備とサービスオブジェクトの生成
# google関連
google_refresh_token = secrets.get("google_refresh_token")
google_client_id = secrets.get("google_client_id")
google_client_secret = secrets.get("google_client_secret")

credentials = Credentials(
    token=None,
    refresh_token=google_refresh_token,
    client_id=google_client_id,
    client_secret=google_client_secret,
    token_uri="https://oauth2.googleapis.com/token",
    scopes=['https://www.googleapis.com/auth/calendar']
)

calendar_service = create_calendar_service(credentials) 
if not calendar_service:
    logout_gmail(mail)
    return { 'statusCode': 500, 'body': json.dumps('エラー: Google Calendar API サービスの作成に失敗しました。') }

ここでは google_calendar_connector.pycreate_calendar_service 関数を使って、Google Calendar API を操作するためのサービスを作成しています。認証情報は Secrets Manager などに格納された値から取得します

抽出された予定情報 (schedule_info) を使ってカレンダーを更新
if schedule_info['start_time'] and schedule_info['end_time'] and schedule_info['title']:

    event = {
        'summary': schedule_info['title'],
        'location': schedule_info['location'],
        'start': {
            'dateTime': datetime.fromisoformat(schedule_info['start_time']).isoformat(timespec='seconds'),
            'timeZone': 'Asia/Tokyo',
        },
        'end': {
            'dateTime': datetime.fromisoformat(schedule_info['end_time']).isoformat(timespec='seconds'),
            'timeZone': 'Asia/Tokyo',
        },
    }

Garoon メールから抽出した予定のタイトル・開始時刻・終了時刻・場所を使って、Google Calendar に登録するイベントオブジェクト(辞書)を作成します。

[削除] 件名 → Google Calendar イベントの削除
if "[削除]" in subject:
    event_id_to_delete = get_events_by_title_and_time(
        calendar_service,
        schedule_info['title'],
        schedule_info['start_time'],
        schedule_info['end_time']
    )                       
    if event_id_to_delete:
        delete_event(calendar_service, event_id_to_delete)
    else:
        print(f"ログ: 削除対象のイベントが見つかりませんでした: {schedule_info['title']} ({schedule_info['start_time']} - {schedule_info['end_time']})")

メール件名に [削除] が含まれている場合は、該当する予定を Google Calendar 上から検索し、存在すれば削除します。

[登録] 件名 → Google Calendar への新規登録
elif "[登録]" in subject:
    existing_event_id = get_events_by_title_and_time(
        calendar_service,
        schedule_info['title'],
        schedule_info['start_time'],
        schedule_info['end_time']
    )
    if not existing_event_id:
        insert_event(calendar_service, event)
    else:
        print(f"ログ: 同じ予定がすでに存在するため、登録をスキップしました (イベントID: {existing_event_id}): {schedule_info['title']} ({schedule_info['start_time']} - {schedule_info['end_time']})")

メール件名に [登録] が含まれている場合は、重複チェックを行った上で、まだ同一の予定が存在しない場合にのみ Google Calendar に登録します。

追加点:

  • UTC 変換後の重複チェック処理を挿入
  • google_calendar_connector から関数をインポート
  • メール件名に応じて [登録] or [削除] を判別

4. 環境変数の設定

以下の環境変数を Lambda 関数に設定してください:

  • AWS_REGION(任意):ap-northeast-1 など
  • SECRET_NAME:Gmail アカウントの認証情報のシークレット名
  • Google Calendar API の認証情報をGmailアカウントの認証情報(SECRET_NAME)へ追加する

5. IAM ロールの更新

Lambda 関数が AWS Secrets Manager にアクセスできるように、IAM ロールに適切な権限(secretsmanager:GetSecretValue)を追加する必要があります。

{
  "Effect": "Allow",
  "Action": [
    "secretsmanager:GetSecretValue"
  ],
  "Resource": "*"
}

※必要に応じて Resource を制限してください。

6. Lambda 関数の再デプロイとテスト

すべてのコードをデプロイ後、Garoon から [登録] または [削除] を含むタイトルのスケジュール通知メールを送信して動作確認を行います。

  • Google カレンダーにイベントが自動登録されているか?
  • 削除リクエストに応じて正しくイベントが消えているか?
  • CloudWatch Logs にエラーが記録されていないか?

を必ずチェックしてください。

まとめ

この第4段階では、抽出した Garoon のスケジュール情報を Google Calendar に登録する処理を実装しました。メールのタイトルに基づいて登録と削除を制御し、重複登録を避ける基本的なロジックも導入しました。

これで無事にシステムから受信したメールからGoogleカレンダーへ自動的に登録されるようになりました。引き続き色々と自動化していこうと思います!

コメント

タイトルとURLをコピーしました