はじめに
hkob の雑記録の第509回目(連続82日目)は、Google Calendar を作成・更新したら、Task を作成・更新する一方向同期する Webhook を設計します。
現在の運用
現在は、macOS の Alfred でタスクとカレンダーを同時に作成していました。以前、GAS を使ってカレンダーから Notion タスクへの一方向同期を作っていましたが、これを Worker で作成してみたいと思いました。
最初の指示
まず、こんな感じで設計書を作成してもらいました。
先日、こちらのページで moodle の課題を作成したときにタスクを生成する webhook を作成しました。
今、カレンダータスクは Alfred でカレンダー登録とタスク登録を同時に実施しています。そこで、Google Calendar にタスクを登録したらタスクを自動で生成する webhook を作成したら、この Alfred が要らなくなると思っています。また、カレンダータスクを変更したら、タスクの日付も変更したいです。Webhook の冪等性のためのプロパティを moodle webhook で作ったので、それがそのまま使えると思います。
今回も作業記録を書く前に、概要の設計書を作成したいです。現在のページに記載をお願いします。少し打ち合わせをして完成させましょう。
この後、決めるべき項目を箇条書きにしてくれました。これらについて、議論しながら以下のような設計書が作成されました。まだまだ荒い内容で、実装の時にいろいろと修正したこともありましたが、この設計書はかなりベースになりました。作成されたものをそのままこの下に貼り付けておきます。
作成された設計書
目的
Google Calendar に「タスク用イベント」を作成/更新/削除したときに、Notion の Tasks にタスク行を自動作成・同期する。Alfred による二重入力を廃止する。
スコープ
- Google Calendar → Notion Tasks の片方向同期(イベントが正)。
- 対象は専用カレンダー Notion に作成/更新されたイベントのみ。
- 変更同期: イベント日時変更 → Notion タスクの Date を更新。
- 取消/削除同期: イベント削除 or キャンセル → Notion タスクを アーカイブする。
前提(既存資産)
- Tasks データソースに冪等性キー用の External ID プロパティが存在する(Moodle webhook で導入済み)。
- Worker(Notion Worker / 外部到達可能)が Webhook を受信できる。
全体アーキテクチャ(案)
- Google Calendar(専用カレンダー Notion)
- イベント作成/更新/削除が発生
- Webhook受信
- Google Calendar API の push notifications(
events.watch)で通知を受ける - 通知をトリガに差分取得(
events.list+syncToken)を実行
- Google Calendar API の push notifications(
- Worker(外部)
- 受信通知を検証(channel/resource など)
- External ID で冪等性チェック
- Notion Tasks を 最小プロパティで upsert(作成/更新/アーカイブ)
監視(push)と差分取得(pull)の補足
- 通知(push)の発火は Google 側が行う。Worker 側で「n分ごとに監視」する必要はない。
- ただし push 通知にはイベント詳細が含まれないため、通知のたびに Worker が
events.list(syncToken付き)で差分を取りに行く(push で起動して pull で確定する)。
運用上の必須事項(手順書に入れる)
events.watchは有効期限(expiration)があるため、期限前に 再登録(再watch) が必要(ここだけ時間ベースの更新作業)。- 初回は
syncTokenが無いので、最初にevents.listを実行してsyncTokenを保存してから差分運用に入る。 - Google Cloud Console は「OAuth クライアント作成/Calendar API 有効化」まで。Webhook の登録は UI ではなく
events.watch呼び出しで行う。- OAuth Playground を使って refresh token を取得する場合、OAuth クライアントは ウェブアプリケーション(Web application) として作成し、redirect URI を設定する。
- 承認済みのリダイレクト URI:
https://developers.google.com/oauthplayground
- 承認済みのリダイレクト URI:
- ※ Secrets(client secret / refresh token)は手順書や実行ログに貼らない(環境変数や workers の env に保存して利用する)。
- OAuth Playground を使って refresh token を取得する場合、OAuth クライアントは ウェブアプリケーション(Web application) として作成し、redirect URI を設定する。
冪等性キー(External ID)の設計
- 形式案:
gcal:<calendarId>:event:<eventId> - 同一 eventId は更新でも不変なので「作成/更新」で同じキーを使う
- 例外: recurring(繰り返し)
- 本カレンダーでは繰り返し予定を使わない前提。もし混入したら 同期対象外として無視する(実装で弾く)。
Tasks 側のマッピング(案)
- Notion 側で同期するのは以下のみ(Notion → Calendar の逆同期はしない)
- Task name:
【予定】<イベントタイトル> - External ID:
gcal:<calendarId>:event:<eventId> - Date:
- 終日:
date:Date:start = YYYY-MM-DD(is_datetime=0) - 時刻あり:
date:Date:start/end(is_datetime=1)
- 終日:
- Link: Google Calendar のイベントURL(取れれば)
- Task name:
- Assignee: 固定(@hkob (private | Notion Ambassador))。同期対象外
イベント判定(タスク用イベントだけ拾う)
採用: 専用カレンダー Notion に入ったものだけ同期
同期仕様(状態遷移)
1) Create(イベント作成)
- External ID で Tasks を検索
- 無ければ新規作成
- あれば更新(冪等性 + 遅延重複対策)
2) Update(イベント更新)
- 対象更新(最小):
- 日時(start/end)→ Tasks.Date
- タイトル → Task name
- Link(取れる場合)→ Link
- Notion 側の他プロパティ(Status/Assignee/他)は一切更新しない
3) Delete / Cancel(イベント削除 or キャンセル)
- 採用: アーカイブ(一覧から消す)
- 実装案:
- API の削除通知(
status=cancelledなど)を受けたら、External ID で該当 Tasks を検索 - 見つかったらページを archive(Notion の archived フラグを立てる)
- API の削除通知(
セキュリティ
- 署名/認証は方式に依存(Pub/Sub push の JWT 検証など)
- Worker 側で許可する calendarId をホワイトリスト化
- リプレイ耐性: eventId + updated timestamp で最新のみ適用(必要なら)
OAuth(refresh token)の取得方針
- refresh token の取得は OAuth 2.0 の標準フロー(Authorize → Exchange)に従う。
- Authorize: ユーザー同意(ブラウザ)→ authorization code を取得
- Exchange: authorization code を token endpoint に交換 → access token / refresh token を取得
- 実装上は refresh token を保存し、以後は refresh token から access token を再発行して
events.list/events.watchを呼び出す。
おわりに
実際にこの設計書に基づき、実装を行い一方向同期までは動きました。まだ一部実装できていない部分を今修正しているところです。明日以降は、実際の実装手順を解説していきます。
https://hkob.notion.site/hkob-16dd8e4e98ab807cbe3cf3cc94cdfe0f?pvs=4hkob.notion.site