「Googleでログイン」を実装してみた(その4)

タム

2024.04.08

6

こんにちは。タムです。

今回はGoogleでログインの4回目ということで、Googleカレンダー連携を行ってみました。

成果物

"yyyy/mm/dd hh:MM:ss ~ yyyy/mm/dd hh:MM:ss hogehoge"というパターンの投稿がされた場合に、

予定に関する投稿とみなしてGoogleカレンダーに登録する仕様です。

実装

ポイント

実装のポイントとしては、

  • Googleカレンダーへのアクセス権限が欲しいので、OAuthのスコープにGoogleカレンダーの読み書き権限を追加する
  • 上記の権限が付与されたアクセストークンを使ってGoogleカレンダーAPIにアクセスしたいので、投稿APIのリクエストボディにアクセストークンを追加する

というところです。

ベースの実装ができているのでほんの少しの修正だけで実現できます。

フロントエンド

diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 910dc9d..ad79e35 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -10,7 +10,7 @@
 		const params = {
 			response_type: 'code',
 			client_id: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com',
-			scope: 'openid profile email',
+			scope: 'openid profile email https://www.googleapis.com/auth/calendar',
 			redirect_uri: 'https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/callback',
 			state: Math.random().toString(32).substring(2),
 			nonce: Math.random().toString(32).substring(2)
@@ -33,7 +33,7 @@
 		const params = {
 			response_type: 'token id_token',
 			client_id: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com',
-			scope: 'openid profile email',
+			scope: 'openid profile email https://www.googleapis.com/auth/calendar',
 			redirect_uri: 'https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/implicit/callback',
 			state: Math.random().toString(32).substring(2),
 			nonce: Math.random().toString(32).substring(2)
diff --git a/src/routes/microposts/+page.svelte b/src/routes/microposts/+page.svelte
index 7dbfe73..8bad0ee 100644
--- a/src/routes/microposts/+page.svelte
+++ b/src/routes/microposts/+page.svelte
@@ -27,7 +27,7 @@
 
     async function post() {
         // 投稿
-        await axios.post('https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/api/microposts', {content: content}, {headers: {Authorization: `Bearer ${id_token}`}});
+        await axios.post('https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/api/microposts', {content: content, access_token: access_token}, {headers: {Authorization: `Bearer ${id_token}`}});
         // リロード
         micropostRes = await axios.get('https://nfk13r40e6.execute-api.ap-northeast-1.amazonaws.com/api/microposts', {headers: {Authorization: `Bearer ${id_token}`}});
         console.log(micropostRes);


バックエンド

diff --git a/app.py b/app.py
index bd01a93..df4073e 100644
--- a/app.py
+++ b/app.py
@@ -3,6 +3,7 @@ import logging
 import requests
 import boto3
 import urllib
+from datetime import datetime, timedelta, timezone
 from chalicelib.models import Microposts
 from chalicelib.utils import login
 
@@ -57,6 +58,27 @@ def post():
     micropost = user.post(req["content"])
     micropost.save()
 
+    if "access_token" in req:
+        start, end, summary = _parse_schedule(req["content"])
+        if start and end and summary:
+            app.log.info(f"googleカレンダーに予定({summary})を作成します")
+            token = req["access_token"]
+            res = requests.post(
+                "https://www.googleapis.com/calendar/v3/calendars/primary/events",
+                headers={"Authorization": f"Bearer {token}"},
+                json={
+                    "summary": summary,
+                    "description": "これはサンプルアプリで作成した予定です",
+                    "start": {"dateTime": start.isoformat()},
+                    "end": {"dateTime": end.isoformat()},
+                },
+            )
+            if res.status_code >= 300:
+                app.log.error(res.json())
+                return Response(body={"message": "server error"}, status_code=500)
+
+            app.log.info(f"googleカレンダーに予定({summary})を作成しました")
+
     return Response(body={"posted_at": micropost.postedAt.isoformat()}, status_code=201)
 
 
@@ -70,3 +92,23 @@ def get_list():
     posts = Microposts.query(user.id)
     res = [post.to_simple_dict() for post in posts]
     return Response(body=res, status_code=200)
+
+
+def _parse_schedule(schedule_str):
+    # 文字列をスペースで分割して情報を取得
+    parts = schedule_str.split()
+
+    if len(parts) < 5 or parts[2] != "~":
+        return None, None, None
+
+    # 日付と時間をパースしてdatetimeオブジェクトに変換
+    start_str = f"{parts[0]} {parts[1]}"
+    end_str = f"{parts[3]} {parts[4]}"
+
+    start = datetime.strptime(start_str, "%Y/%m/%d %H:%M").replace(tzinfo=timezone(timedelta(hours=9)))
+    end = datetime.strptime(end_str, "%Y/%m/%d %H:%M").replace(tzinfo=timezone(timedelta(hours=9)))
+
+    # サマリを取得
+    summary = " ".join(parts[5:])
+
+    return start, end, summary


さいごに

まとめ

今回はGoogleカレンダー権限をスコープに追加してアクセスする実装を試してみました。

今回はAPIの中で直接GoogleカレンダーAPIを叩いてしまっていますが、

Googleカレンダーに登録するまでユーザを待たせてしまうのも微妙なので、

非同期で登録するようにしたほうがいいと思いました。

その場合は非同期キューにアクセストークンを含ませたリクエストを登録する形になりそうです。

今回、IDトークンはAuthorizationヘッダに含ませていますが、

アクセストークンはリクエストボディに含ませました。

なんか不格好な気がしますが、一般的なOIDCを利用したAPIの設計はどうするのが普通なんでしょうか・・・


今後やってみたいこととしては、

  • トークンのリフレッシュに対応する
  • ログアウトに対応する
  • 他のIDPを利用する

などがあります。

際限なく出てきますね・・・

また実装でき次第記事にしようと思います。

この記事をシェアする