Google Calendar上の予定の変更を監視する

## Google Calendar API: Push Notifications

特定のCalendar上に予定が追加されたり、予定に変更（削除も含む）があった際に通知を受け取ることができる機能 Push Notification があります。

* https://developers.google.com/google-apps/calendar/v3/push

このAPIを使うと、予定に変更があった際にWebhookにて変更があったことを知ることができますが、Webhookにて通知された内容だけでは「何かの予定に対して変更があった」ことしかわからないので、実用には不十分です。このPush Notification（`Events: watch`）に加えて、`Events: list`という予定の一覧を取得するAPIを組み合わせます。`Events: list` APIには`syncToken`というパラメータがあり、これが予定の変更を監視する肝となります。[^1]

[^1]: 定期起動のポーリングで変更を検知するだけで良い場合は、`Events: list` + `syncToken` でも十分です。

* Events: watch
    * https://developers.google.com/google-apps/calendar/v3/reference/events/watch
* Events: list
    * https://developers.google.com/google-apps/calendar/v3/reference/events/list

## 監視をする仕組み

詳細は長くなるので、ここではざっくりとしたフローを書いておきます。

### `Channel` の作成

まずは特定のカレンダーを指定して`Events: watch`を実行してカレンダーに対する通知を行う`Channel`（監視対象と通知先の組み合わせ） を作成します。

### Webhook

Webhookが呼ばれるタイミングが二種類あり、ヘッダの値で区別します。

* ヘッダ値: `X-Goog-Resource-State`=`sync`
    * `Channel`が作成されたという通知としてWebhookがキックされたので、対象カレンダーに対して一度 `Events: list`を実行して、レスポンスに含まれる`nextSyncToken`と、`Channel`の期限（ヘッダ: `X-Goog-Expiration`）、監視対象カレンダーのID（ヘッダ：`X-Goog-Resource-ID`）をカレンダーのIDをキーにして保存します。
* ヘッダ値: `X-Goog-Resource-State`=`exsits`
    * こちらが本筋の「予定に変更があった」という通知なので、保存された`nextSyncToken`を使って`Events: list`を実行して変更があった予定の一覧を取得し、`nextSyncToken`を実行結果のものに書き換えます。[^2]

[^2]: 処理中に何かが失敗した場合は Webhook のレスポンスとして500などを返せば、Google Calendar側がexponential backoffを使ったリトライをしてくれます。

### 定期実行処理

またWebhookドリブンなフローとは別に、`Channel`の期限への対応が必要です。

* 定期的に、保存したカレンダーの期限をチェックし、期限が近いものは`Events: watch`を実行して新しい`Channel`を作成してから、`Channel: stop`を使って古い方の`Channel`を削除します。


## 準備

この機能を利用するには以下の準備が必要です。

* Webhookするための https でアクセスできるホスト
    * 自己証明書はダメ
* Google Developer Console上のプロジェクト
    * Calendar APIを有効にし、ClientIDを作成する
    * Webhookとして利用するホストをGoogle Developer Consoleの **API & Auth - Push** でドメインのWhite listに登録する[^4]
        * https://developers.google.com/google-apps/calendar/v3/push#registering

[^4]: 認可を求めるアプリケーションのホワイトリストにWebhookとして使用するホストが登録されていなければならない、という制限のため、`Events: watch` APIは「API Explorer（というアプリケーション）に対する認可」の権限では正しく動作しません。

## 各APIの詳細

### `Events: watch`

予定を監視したいCalendarのidを指定して、`Events: watch`を実行すると、監視対象と通知先をセットにした`Channel`が作成されます。[^5]

パラメータは以下のとおりです。

* `id`: リファレンスにはUUIDを指定するよう書かれていますが、実際には特定の記号文字が制限されているだけなので、人間に読みやすいIDでも構いません。[^6]
* `token`: 自由な値が設定でき、省略も可能です。何か設定しておくと、Webhookが呼ばれる場合にもヘッダに含まれます。カレンダーのオーナーアカウント等を入れておくと便利かもしれません。
* `type`: "web_hook"固定です。他にどんな種類があるのかはわかりません。
* `expiration`: 監視が有効な期間（日時）です。[^7]
* `address`: Webhookとしてリクエストを投げてもらうURLを設定します。Developer ConsoleのプロジェクトのPush先としてホワイトリストに追加されていないホストを設定した場合は、`401 Unauthorized`（詳細なメッセージなし）が返されます。

このAPIを実行したレスポンスの書式はリファレンスにて確認できますが、`resourceId` 以外は意味がありません。このAPIが成功した場合はWebhookとして登録したURLに対してリクエストが投げられますから、そちらで初期処理すればよいです。

[^5]: 現在、アプリケーションで作成したChannelの一覧を取得することはできません。Channelの一覧を管理する必要がある場合は、watch APIを実行した際のidを保存しておくか、通知が来た時の`X-Goog-Channel-ID`を参考にするか、意味のある値にするか、といった管理をしなければなりません。
[^6]: ただし、有効な`Channel`で使用している`id`と重複すると`400 channelIdNotUnique`が返されます。
[^7]: 省略した場合は1週間、最大だと24日程度が指定できます。大きすぎる値を設定使用すると`400 pushInvalidTtl`が返されます。


### `Events: list` + `syncToken`

`Events: list`はカレンダー内の予定を取得するAPIですが、実行するとレスポンスに`nextSyncToken`が返されます。その値を次回のAPI実行時の`syncToken`として指定することで、そのトークンを取得した時よりも後に変更された内容に絞って取得することができます。
変更内容として予定が削除された場合であっても、その予定の`id`と`status: canceled`がレスポンスに含まれるので便利です。[^1]

### `Channel: stop`

作成した`Channel`を削除するAPIで、通知を受け取る必要がなくなったり、期限の更新のために新しい`Channel`を作成した後に実行します。

* `id`: 削除対象の`Channel`のid[^8]
* `resourceId`: 削除対象の`Channel`に含まれるresouceId

[^8]: 存在しないidを指定した場合は`404 notFound`が返されます。


## 他

### watch対象について

watchできる監視対象として`Events: watch`を紹介しましたが、Google Calendarはひとつのアカウントに複数のカレンダー（`CalendarList`）を作成できます。それ自身の変更を監視したり、カレンダーの設定を変更を監視するwatchなども用意されています。

* `CalendarList: watch`
    * https://developers.google.com/google-apps/calendar/v3/reference/calendarList/watch
* `Acl: watch`
    * https://developers.google.com/google-apps/calendar/v3/reference/acl/watch
* `Settings: watch`
    * https://developers.google.com/google-apps/calendar/v3/reference/settings/watch

### Google App Engineを使用する場合

<del>`*.appspot.com` なドメインの所有者であることを証明できないため、SSL for Custom Domainを使用することになります。現在であれば、Google Appsへの登録が必要ということになります（2015/06）。</del>→どうやらドメインの所有確認の対象として *https://myappid.appspot.com* と https をつければ所有確認ができ、appspot.comなドメインもそのまま利用できるようです。

0. Developer Consoleの **Compute - App Engine - Settings**で、Custom Domainとして追加する
0. admin.google.com の **Apps - Additional Google Services**で**Add services**をクリックし、該当のGAEアプリケーションのIDを追加する
0. admin.google.com の**Security - SSL for Custom Domains**で証明書を登録し、URLを登録する(SNIで良い)