Box Webhookでファイル操作をイベント通知してみた

### はじめに

Box上のファイルが更新されたら、即座に検知し特定アクションを行いたい場合があります。
Webhookの仕組みを用いて、Boxにイベントトリガーを設定し、イベント通知してみました。

### 構成図

https://ja.developer.box.com/guides/webhooks/v2/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/272c64d5-556b-5485-fce7-f655ebbb7f5e.png)


### Webhook(ウェブフック)とは

- Webアプリケーションで特定のイベントが発生したら別のWebアプリケーションに通知を発行する仕組み
- Web開発でカスタムコールバックを用いてWebアプリケーションの動作を追加または変更するための方法
- リクエストは、HTTP POSTリクエストとして行われる
- フォーマットには通常JSONが利用される

### Box Webhook V2とは

https://ja.developer.box.com/guides/webhooks/v2/

イベントトリガーが発動されたら、指定URLに`HTTP POST`リクエストするための
`Webhook API`が用意されています。

- Webhookの作成
- Webhookの更新
- Webhookの削除
- Webhook署名検証
    - Boxから送信されたWebhookペイロードが改ざんされていないことを確認する
    - 署名により、中間者攻撃またはリプレイ攻撃による影響が大幅に軽減される

### Webhookを使用するためのBoxの設定

#### 認証方法の設定

- Boxアプリケーション設定画面で、`OAuth 2.0 with JSON Web Tokens (Server Authentication)`を選択

![webhook_authentication_method.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/762c5d8b-1598-3c2b-1570-30c49e9bfe0f.jpeg)

- Boxアプリケーション設定画面で、アプリ設定Jsonファイルをダウンロード

![webhook_app_settings_json.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/71c3af44-76b9-a979-116c-82897862626b.jpeg)


#### `Manage Webhooks`スコープを設定

- Boxアプリケーション設定画面で、`Application Scopes`セクションの`Manage Webhooks`にチェックを入れ、設定を保存

![step1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/ddab8286-35f0-3608-5ffc-5d18603590dd.jpeg)

- Boxアプリケーション承認画面で、`Review and Submit`ボタンをクリックし承認申請を行う

![step2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/2994ed36-f507-933f-2a0b-173cbee0e7d5.jpeg)



- Box管理者が、カスタムアプリ管理画面で、アプリケーションの承認申請を許可(管理者のみ実施可能)

![step3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/93634de1-8a5a-d784-89a5-f3e09ac1a483.jpeg)


### Webhook操作用のPythonサンプルプログラム

```python
"""BOX Webhook操作プログラム"""
# 前提としてboxsdkのインストールが必要
#   pip install boxsdk

from boxsdk import Client, JWTAuth

# Boxからダウンロードしたアプリ設定Jsonデータ
CONNECTION_INFO = {
    "boxAppSettings": {
        "clientID": "xxx",
        "clientSecret": "xxx",
        "appAuth": {
            "publicKeyID": "xxx",
            "privateKey": "xxx",
            "passphrase": "xxx",
        },
    },
    "enterpriseID": "xxx",
}


class WebhookOper:
    """Webhookの操作"""

    def __init__(self, connection_info: dict):
        config = JWTAuth.from_settings_dictionary(connection_info)
        self.client = Client(config)

    def list_webhookids(self):
        """全webhook_idリストを取得"""
        webhooks = self.client.get_webhooks()
        for webhook in webhooks:
            print(f"The webhook ID is {webhook.id} and the address is {webhook}")

    def get_folder_id(self, webhook_id: str):
        """webhook_idに紐づくfolder_idを取得"""
        webhook = self.client.webhook(webhook_id=str(webhook_id)).get()
        print(webhook.target)

    def create_webhook(self, folder_id: str, notify_url: str):
        """フォルダにwebhook作成(ファイル操作イベントトリガーを指定)"""
        folder = self.client.folder(folder_id=folder_id)
        webhook = self.client.create_webhook(
            folder,
            [
                "FILE.UPLOADED",
                "FILE.DELETED",
                "FILE.TRASHED",
                "FILE.RESTORED",
                "FILE.COPIED",
                "FILE.MOVED",
                "FILE.RENAMED",
            ],
            notify_url,
        )
        print(f"Webhook ID is {webhook.id} and the address is {webhook.address}")

    def delete_webhook(self, webhook_id: str):
        """webhookを削除"""
        self.client.webhook(webhook_id=webhook_id).delete()
        print(f"webhook_id={webhook_id} has been deleted")


if __name__ == '__main__':
    # Webhook操作
    who = WebhookOper(CONNECTION_INFO)
    folder_id = 'xxx'  # Webhook設定対象のフォルダID
    notify_url = 'https://xxx'  # Webhook通知先URL

    # Webhookをfolder_idに作成
    who.create_webhook(folder_id, notify_url)

    # Webhook一覧を表示
    who.list_webhookids()

```

### Webhookイベント通知のサンプルデータ

Boxフォルダ移動により通知される`FOLDER.MOVED`イベントのリクエストボディサンプルです。

```
{
   "type": "webhook_event",
   "id": "xxx",
   "created_at": "2022-12-30T05:28:48-07:00",
   "trigger": "FOLDER.MOVED",
    "webhook": {
        "id": "770111431",
        "type": "webhook"
    },
    "created_by": {
        "type": "user",
        "id": "20188107977",
        "name": "name1",
        "login": "name1@ari-jp.com"
    },
    "source": {
        "id": "169775002763",
        "type": "folder",
        "sequence_id": "3",
        "etag": "3",
        "name": "test2",
        "created_at": "2022-12-30T01:17:18-07:00",
        "modified_at": "2022-12-30T05:01:55-07:00",
        "description": "",
        "size": 78666,
        "path_collection": {
            "total_count": 3,
            "entries": [
                {
                    "type": "folder",
                    "id": "0",
                    "sequence_id": null,
                    "etag": null,
                    "name": "All Files"
                },
                {
                    "type": "folder",
                    "id": "169370964125",
                    "sequence_id": "0",
                    "etag": "0",
                    "name": "私のBox Notes"
                },
                {
                    "type": "folder",
                    "id": "169780883554",
                    "sequence_id": "1",
                    "etag": "1",
                    "name": "test3"
                }
            ]
        },
        "created_by": {
            "type": "user",
            "id": "20188107977",
            "name": "name1",
            "login": "name1@ari-jp.com"
        },
        "modified_by": {
            "type": "user",
            "id": "20188107977",
            "name": "name1",
            "login": "name1@ari-jp.com"
        },
        "trashed_at": null,
        "purged_at": null,
        "content_created_at": "2022-12-30T01:17:18-07:00",
        "content_modified_at": "2022-12-30T05:01:55-07:00",
        "owned_by": {
            "type": "user",
            "id": "20188107977",
            "name": "name1",
            "login": "name1@ari-jp.com"
        },
        "shared_link": null,
        "folder_upload_email": null,
        "parent": {
            "type": "folder",
            "id": "169780883554",
            "sequence_id": "1",
            "etag": "1",
            "name": "test3"
        },
        "item_status": "active"
    },
    "additional_info": {
        "after": {
            "id": "169780883554",
            "type": "folder"
        },
        "before": {
            "id": "169370964125",
            "type": "folder"
        }
    }
}
```

### (余談)Webhookのイベント通知で、HTTPリクエストのヘッダーとボディの長さに制限がないか問い合わせた結果

GitHubプロジェクト`box-python-sdk`にissueを投げてみました。
https://github.com/box/box-python-sdk/issues/774

- 質問
```
Is there a limitation on length of HTTP Header and Body 
included in a notification from V2 webhooks?
```

- 回答
boxsdk API側では制限を設けておらず、HTTP仕様に準拠するとのことでした。
```
I've asked service owners but they have not specified any limit.
 So this response follow HTTP specification.
```

- ちなみに、`RFC 7230`の仕様では、HTTPヘッダーのフィールド長に制限は設けられていないようです。

https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.5
```
3.2.5.  Field Limits

   HTTP does not place a predefined limit on the length of each header
   field or on the length of the header section as a whole, as described
   in Section 2.5.
```

### おわりに
Box上のファイル操作を、Webhookを使ってイベント通知してみました。
受信イベントは、ビジネスプロセス(分析や監視)に活用できそうです。