Calendar Webhook の設計 : hkob の雑記録 (509)

はじめに

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 を受信できる。

全体アーキテクチャ(案)

  1. Google Calendar(専用カレンダー Notion)
    • イベント作成/更新/削除が発生
  2. Webhook受信
    • Google Calendar API の push notifications(events.watch)で通知を受ける
    • 通知をトリガに差分取得(events.list + syncToken)を実行
  3. Worker(外部)
    • 受信通知を検証(channel/resource など)
    • External ID で冪等性チェック
    • Notion Tasks を 最小プロパティで upsert(作成/更新/アーカイブ)

監視(push)と差分取得(pull)の補足

  • 通知(push)の発火は Google 側が行う。Worker 側で「n分ごとに監視」する必要はない。
  • ただし push 通知にはイベント詳細が含まれないため、通知のたびに Worker が events.listsyncToken 付き)で差分を取りに行く(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
    • ※ Secrets(client secret / refresh token)は手順書や実行ログに貼らない(環境変数や workers の env に保存して利用する)。

冪等性キー(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(取れれば)
  • 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 フラグを立てる)

セキュリティ

  • 署名/認証は方式に依存(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