前回の第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.py
の lambda_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.py
の create_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カレンダーへ自動的に登録されるようになりました。引き続き色々と自動化していこうと思います!
コメント