Microsoft Graph を使ってみよう : 変更通知 (Webhook)

# Microsoft Graph の変更通知
Webhook を使った一般的な仕組みで、One Drive にファイルを保存した時や、新しいユーザーがグループに追加されたなどのイベントを、任意の API に通知できます。

現在は以下のリソースで通知がサポートされています。

- Messages (メール)
- イベント (予定)
- 連絡先
- ユーザー
- グループ
- グループ会話
- SharePoint サイトに関連付けられたドライブを含む、OneDrive で共有されるコンテンツ
- ユーザーの個人用 OneDrive フォルダー

以下 3 つの処理で変更通知を利用します。

1\. Webhook でイベントが発生した際にくる通知を処理
2\. サブスクリプションの登録
3\. 有効期限が切れる前にサブスクリプションの更新

# Webhook の作成
まずは通知を受け取るための Webhook を用意します。今回は Functions App を使っていきます。ここでは 60 分だけ Functions App を試せるサイトを使います。

1\. https://azure.microsoft.com/ja-jp/try/app-service/ にアクセスし、Functions App を選択。「Functions を試す」をクリック。

<img width="715" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/d6c261d5-d595-cf69-8caf-687ed3394550.png">

2\. 「Webhook ⁺ API」、「C#」を選択して「Create this function」をクリック。

<img width="828" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/77d3119a-13ce-1241-592a-74323f9fcf30.png">

3\. 任意のプロバイダーでサインイン。ここでは GitHub を利用。

<img width="850" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/6944974f-aefe-7d91-51df-db7704d112a6.png">

4\. Functions が準備されたら以下のコードで中身を差し替え。Subscription が作成された時点で validationToken が渡されるためそれを返信。また Content の中身をログに出力。詳細は [サブスクリプションを作成する](https://developer.microsoft.com/ja-jp/graph/docs/api-reference/v1.0/api/subscription_post_subscriptions) を参照。

```csharp:C#
using System.Net;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info("C# HTTP trigger function processed a request.");
    log.Info(await req.Content.ReadAsStringAsync()); 
    // parse query parameter
    string token = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "validationToken", true) == 0)
        .Value;

    if(token != null){
        log.Info(token);
    }
    return token != null
        ? req.CreateResponse(HttpStatusCode.OK, token)
        : req.CreateResponse(HttpStatusCode.OK);
}
```
4\. Function が作成されるので「Get function URL」をクリックして Webhook アドレスを取得。

<img width="879" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/ce26a9e5-5495-9bd6-5a85-e5195f3e1bca.png">

# サブスクリプションの登録

今回は Graph エクスプローラーを使って登録します。

- Webhook は上記で作成したものを利用
- 予定が作成または削除された場合に通知

[Graph エクスプローラー](https://developer.microsoft.com/ja-jp/graph/graph-explorer) にアクセスしてサインイン。以下リクエストを実行。

※ expirationDateTime は現在から 3 日以内に設定

POST: https://graph.microsoft.com/v1.0/me/subscriptions

```json:Body
{
  "changeType": "created,deleted",
  "notificationUrl": "https://Functions065bac0f.azurewebsites.net/api/HttpTriggerCSharp1?code=7e97ec30126a1b0c3002c9ff06edaa5a12ef6835",
  "resource": "me/events",
  "expirationDateTime": "2018-05-16T23:23:59Z",
  "clientState": "mywebhooktest"
}
```

結果としてサブスクリプション ID が返ります。

```json:応答
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
    "id": "a5d287ef-0d54-4116-86e8-a45bc1b551a8",
    "resource": "me/events",
    "changeType": "created,deleted",
    "clientState": "mywebhooktest",
    "notificationUrl": "https://Functions065bac0f.azurewebsites.net/api/HttpTriggerCSharp1?code=7e97ec30126a1b0c3002c9ff06edaa5a12ef6835",
    "expirationDateTime": "2018-05-16T23:23:59Z"
}
```

**SDK を使った場合**

```csharp:C#
await graphClient.Subscriptions.Request().AddAsync(new Subscription()
{
    ChangeType = "created, deleted",
    NotificationUrl = "https://Functions065bac0f.azurewebsites.net/api/HttpTriggerCSharp1?code=7e97ec30126a1b0c3002c9ff06edaa5a12ef6835",
    Resource = "me/events",
    ExpirationDateTime = new DateTimeOffset(2018, 5, 16, 23, 23, 59, new TimeSpan(9)),
    ClientState = "mywebhooktest"
});
```

# 動作のテスト

ここで一旦動作をテストしてみます。

1\. 予定を Outlook で作成。

<img width="787" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/32aa9da5-dcb0-2e64-f889-4cefdaac5df9.png">

2\. Functions App のログから Webhook に通知された内容を確認。

- changeType が created のため作成
- resource に実際のアイテムのアドレス
- 詳細は resourceData にあるが、詳細は含まれない。

```json:受信したBody
{
    "value": [
        {
            "subscriptionId": "a5d287ef-0d54-4116-86e8-a45bc1b551a8",
            "subscriptionExpirationDateTime": "2018-05-16T23:23:59+00:00",
            "changeType": "created",
            "resource": "Users/6746d79d-8c77-4eec-a61f-cce6beb162f7/Events/AAMkAGExMTQzMTU1LTE3ZGYtNDU1Ny1iMzczLWQ1MmU0YjQxNmEzNgBGAAAAAACFQuNJIe4VSIJxcTi2cbakBwBvNJgFk4PxRY7qUEy18VJ6AAAAAAENAABvNJgFk4PxRY7qUEy18VJ6AAAD3lKsAAA=",
            "resourceData": {
                "@odata.type": "#Microsoft.Graph.Event",
                "@odata.id": "Users/6746d79d-8c77-4eec-a61f-cce6beb162f7/Events/AAMkAGExMTQzMTU1LTE3ZGYtNDU1Ny1iMzczLWQ1MmU0YjQxNmEzNgBGAAAAAACFQuNJIe4VSIJxcTi2cbakBwBvNJgFk4PxRY7qUEy18VJ6AAAAAAENAABvNJgFk4PxRY7qUEy18VJ6AAAD3lKsAAA=",
                "@odata.etag": "W/\"DwAAABYAAABvNJgFk4PxRY7qUEy18VJ6AAAD3WON\"",
                "id": "AAMkAGExMTQzMTU1LTE3ZGYtNDU1Ny1iMzczLWQ1MmU0YjQxNmEzNgBGAAAAAACFQuNJIe4VSIJxcTi2cbakBwBvNJgFk4PxRY7qUEy18VJ6AAAAAAENAABvNJgFk4PxRY7qUEy18VJ6AAAD3lKsAAA="
            },
            "clientState": "mywebhooktest"
        }
    ]
}
```
3\. Graph エクスプローラーでリソースにアクセスし、予定が取れることを確認。

<img width="652" alt="Sketch.png" src="https://qiita-image-store.s3.amazonaws.com/0/214116/e7b3dd48-68ab-0680-7a6d-2a4ae758aec0.png">

4\. 同様に削除もテスト。changeType が deleted で削除イベントであることが分かる。また削除されたリソースの情報も特定可能。

```json:受信したBody
{
    "value": [
        {
            "subscriptionId": "a5d287ef-0d54-4116-86e8-a45bc1b551a8",
            "subscriptionExpirationDateTime": "2018-05-16T23:23:59+00:00",
            "changeType": "deleted",
            "resource": "Users/6746d79d-8c77-4eec-a61f-cce6beb162f7/Events/AAMkAGExMTQzMTU1LTE3ZGYtNDU1Ny1iMzczLWQ1MmU0YjQxNmEzNgBGAAAAAACFQuNJIe4VSIJxcTi2cbakBwBvNJgFk4PxRY7qUEy18VJ6AAAAAAENAABvNJgFk4PxRY7qUEy18VJ6AAAD3lKsAAA=",
            "resourceData": {
                "@odata.type": "#Microsoft.Graph.Event",
                "@odata.id": "Users/6746d79d-8c77-4eec-a61f-cce6beb162f7/Events/AAMkAGExMTQzMTU1LTE3ZGYtNDU1Ny1iMzczLWQ1MmU0YjQxNmEzNgBGAAAAAACFQuNJIe4VSIJxcTi2cbakBwBvNJgFk4PxRY7qUEy18VJ6AAAAAAENAABvNJgFk4PxRY7qUEy18VJ6AAAD3lKsAAA=",
                "@odata.etag": "W/\"CQAAAA==\"",
                "id": "AAMkAGExMTQzMTU1LTE3ZGYtNDU1Ny1iMzczLWQ1MmU0YjQxNmEzNgBGAAAAAACFQuNJIe4VSIJxcTi2cbakBwBvNJgFk4PxRY7qUEy18VJ6AAAAAAENAABvNJgFk4PxRY7qUEy18VJ6AAAD3lKsAAA="
            },
            "clientState": "mywebhooktest"
        }
    ]
}
```

# サブスクリプションの更新

Graph エクスプローラーを使って更新してみます。

サブスクリプションの ID を指定した URL に対して、Patch として送信。ここでも有効な expirationDateTime は 3 日以内です。

PATCH: https://graph.microsoft.com/v1.0/me/subscriptions/{id}

```json:Body
{
   "expirationDateTime":"2018-05-17T00:00:00Z"
}
```

結果として更新された expirationDateTime が返ります。
<img width="703" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/1d6f6266-bc4a-a255-bda1-0c7ea5f2dfee.png">

**SDK を使った場合**

```csharp:C#
await graphClient.Subscriptions["a5d287ef-0d54-4116-86e8-a45bc1b551a8"].Request().UpdateAsync(new Subscription()
{
    ExpirationDateTime = new DateTimeOffset(2018, 5, 17, 0, 0, 0, new TimeSpan(9))
});
```

# サブスクリプションの削除

サブスクリプションの ID を指定した URL に対して、Delete を送信すれば削除完了。

DELETE: https://graph.microsoft.com/v1.0/me/subscriptions/{id}

<img width="705" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/84afa2ac-768d-29e6-8954-1c0526f19df1.png">

**SDK を使った場合**

```csharp:C#
await graphClient.Subscriptions["a5d287ef-0d54-4116-86e8-a45bc1b551a8"].Request().DeleteAsync();
```

# サブスクリプションのリスト

現在のサブスクリプション一覧はベータ機能として提供されています。

GET: https://graph.microsoft.com/beta/subscriptions

<img width="704" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/4d2fb37d-8466-0e4b-a659-2ca8de275c4e.png">

**SDK を使った場合**

```csharp:C#
var subscriptions = await graphClient.Subscriptions.Request().GetAsync();
```

# Tips

Webhook を使う場合の Tips をいくつか紹介します。

**clientState**
Webhook が Microsoft Graph から実行されたかを検証するためには、指定した clientState の値を検証します。

**サブスクリプションの更新**
かならず有効期限内に実施する。期限が切れた場合更新ができないため、新規作成となる。

**詳細の取得**
変更通知には、変更されたアイテムの詳細は入っていないため、別途必要に応じて詳細を所得するが、通知に対しては極力早く 200 OK を返したほうがいいため、一旦キューに通知情報を格納し、キューをトリガーとして別のプログラムで詳細の取得や処理を行う。

**デルタクエリ**
変更通知が来た場合、個別のアイテムを取得するためにリソースをダイレクトに呼び出してもいいが、シナリオによってはデルタクエリを利用して、差分を一括取得することも可能。

# まとめ

ポーリングで差分を取るのは効率が悪く、リアルタイムの更新も出来ませんが、Webhook ならより効率のいいプログラムが書けます。通知には実際のデータは含まれませんので、[差分クエリ](https://qiita.com/kenakamu/items/63a8d5d136223ad1939a)も同時に活用してください。

[目次へ戻る](https://qiita.com/kenakamu/items/6a1ca508dc093779eefa)

# 参照
[Microsoft Graph の Webhooks での作業](https://developer.microsoft.com/ja-jp/graph/docs/api-reference/v1.0/resources/webhooks)
[Microsoft Graph の Webhooks での作業(英語) ※最新情報はこちらを確認してください※](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/webhooks)
[リソースの種類別のサブスクリプションの最大の長さ](https://developer.microsoft.com/ja-jp/graph/docs/api-reference/v1.0/resources/subscription#maximum-length-of-subscription-per-resource-type)
[サブスクリプション](https://developer.microsoft.com/ja-jp/graph/docs/api-reference/v1.0/resources/subscription)