# 機能設計書 142-Webhookイベント

## 概要

本ドキュメントは、ChatwootにおけるWebhookイベント送信機能の設計書である。WebhookListenerがシステムイベント（会話作成、メッセージ送受信など）を監視し、登録されたWebhookエンドポイントへ非同期でペイロードを送信する仕組みを提供する。

### 本機能の処理概要

**業務上の目的・背景**：Chatwoot内で発生する様々なイベント（会話の作成・更新、メッセージの送受信、連絡先の変更など）を外部システムにリアルタイムで通知する必要がある。この機能により、外部システムとの連携を自動化し、業務プロセスの効率化を実現する。例えば、新規会話が作成された際にSlackへ通知したり、メッセージ受信時にCRMシステムにデータを同期したりすることが可能になる。

**機能の利用シーン**：
- 会話が作成・更新・ステータス変更された際に外部システムへ通知
- メッセージが作成・更新された際にログ収集システムへ送信
- 連絡先情報が変更された際にCRMシステムと同期
- インボックスが作成・更新された際に管理システムへ通知
- Webウィジェットでカスタムイベントが発生した際に分析ツールへ送信

**主要な処理内容**：
1. BaseListenerを継承したWebhookListenerがイベントを受信
2. イベントタイプに応じたペイロードを構築
3. アカウント単位のWebhookとAPIインボックスのWebhookを区別して処理
4. WebhookJobを介した非同期でのHTTP POST送信

**関連システム・外部連携**：
- 登録されたWebhook URLへHTTP POSTでJSONペイロードを送信
- RestClientによるHTTP通信
- Sidekiqによる非同期ジョブ処理

**権限による制御**：イベント送信自体はシステムが自動実行するため、権限による制御は存在しない。ただし、Webhookの登録・管理には管理者権限が必要。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 67 | Webhook設定画面 | 参照画面 | イベント購読の設定（subscriptions）によりどのイベントを送信するか決定 |

## 機能種別

データ連携 / イベント処理

## 入力仕様

### 入力パラメータ（イベントデータ）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| event | Object | Yes | Wisper/Railsイベントオブジェクト | - |
| event.data[:conversation] | Conversation | 条件付き | 会話オブジェクト | 会話系イベント時 |
| event.data[:message] | Message | 条件付き | メッセージオブジェクト | メッセージ系イベント時 |
| event.data[:contact] | Contact | 条件付き | 連絡先オブジェクト | 連絡先系イベント時 |
| event.data[:inbox] | Inbox | 条件付き | インボックスオブジェクト | インボックス系イベント時 |
| event.data[:contact_inbox] | ContactInbox | 条件付き | コンタクトインボックス | webwidget_triggered時 |
| event.data[:changed_attributes] | Hash | No | 変更された属性 | 更新系イベント時 |

### 対応イベント一覧

| イベント名 | 説明 | ペイロードソース |
|-----------|------|-----------------|
| conversation_status_changed | 会話ステータス変更 | conversation.webhook_data |
| conversation_updated | 会話更新 | conversation.webhook_data |
| conversation_created | 会話作成 | conversation.webhook_data |
| message_created | メッセージ作成 | message.webhook_data |
| message_updated | メッセージ更新 | message.webhook_data |
| webwidget_triggered | Webウィジェットイベント | contact_inbox.webhook_data |
| contact_created | 連絡先作成 | contact.webhook_data |
| contact_updated | 連絡先更新 | contact.webhook_data |
| inbox_created | インボックス作成 | Inbox::EventDataPresenter |
| inbox_updated | インボックス更新 | Inbox::EventDataPresenter |
| conversation_typing_on | タイピング開始 | conversation.webhook_data + user.webhook_data |
| conversation_typing_off | タイピング終了 | conversation.webhook_data + user.webhook_data |

### 入力データソース

Wisperイベントバスからのイベント配信

## 出力仕様

### 出力データ（Webhookペイロード）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| event | String | イベント名（例: "message_created"） |
| changed_attributes | Array | 変更された属性のリスト（更新系イベント時） |
| (イベント固有データ) | Object | 各オブジェクトのwebhook_dataの内容 |

### 出力先

- 登録されたWebhook URL（HTTP POST）
- APIインボックスのWebhook URL（Channel::Api経由）

## 処理フロー

### 処理シーケンス

```
1. イベント発生（Wisperイベントバス経由）
   └─ Rails各モデルで定義されたイベントがpublishされる

2. WebhookListener受信
   └─ 対応するメソッド（例: message_created）が呼び出される

3. ペイロード構築
   └─ イベントからオブジェクトを抽出
   └─ webhook_dataメソッドでペイロード取得
   └─ イベント名とchanged_attributesをマージ

4. Webhook配信
   └─ deliver_webhook_payloads呼び出し
       ├─ deliver_account_webhooks（アカウントWebhook）
       │      └─ 購読イベントマッチング
       │      └─ WebhookJob.perform_later
       └─ deliver_api_inbox_webhooks（APIインボックス）
              └─ Channel::Api判定
              └─ WebhookJob.perform_later

5. 非同期送信（WebhookJob）
   └─ Webhooks::Trigger.execute
       └─ RestClient POST リクエスト
```

### フローチャート

```mermaid
flowchart TD
    A[イベント発生] --> B[WebhookListener受信]
    B --> C[オブジェクト抽出]
    C --> D[ペイロード構築]
    D --> E[deliver_webhook_payloads]
    E --> F[deliver_account_webhooks]
    E --> G[deliver_api_inbox_webhooks]
    F --> H{購読イベントに含まれる?}
    H -->|Yes| I[WebhookJob.perform_later]
    H -->|No| J[スキップ]
    G --> K{Channel::Api?}
    K -->|Yes| L{webhook_url存在?}
    K -->|No| M[スキップ]
    L -->|Yes| N[WebhookJob.perform_later]
    L -->|No| M
    I --> O[Webhooks::Trigger.execute]
    N --> O
    O --> P[HTTP POST送信]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-142-01 | サブスクリプションフィルタ | Webhookは購読しているイベントのみ受信する | アカウントWebhook送信時 |
| BR-142-02 | APIインボックス自動送信 | Channel::Apiのインボックスは自動的にwebhook_urlへ送信 | deliver_api_inbox_webhooks時 |
| BR-142-03 | メッセージ送信可能判定 | message.webhook_sendable?がtrueの場合のみ送信 | message_created/updated時 |
| BR-142-04 | 空変更スキップ | changed_attributesが空の場合は送信をスキップ | contact_updated/inbox_updated時 |
| BR-142-05 | アカウントタイプ限定 | account_typeのWebhookのみがアカウント単位イベントを受信 | contact/inbox系イベント時 |

### 計算ロジック

特になし

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| イベント処理 | webhooks | SELECT | アカウントのWebhook一覧取得 |
| イベント処理 | channels | SELECT | インボックスのチャネル情報取得 |

### テーブル別操作詳細

#### webhooks

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | account_id AND webhook_type = 'account_type' | アカウントWebhook取得 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | HTTP接続エラー | Webhook URLへの接続失敗 | ログ出力、agent_bot_webhookの場合は会話をopen状態に変更 |
| - | タイムアウト | Webhook応答がタイムアウト | ログ出力、WEBHOOK_TIMEOUT設定で調整可能 |
| - | 不正なURL | Webhook URLが不正 | ログ出力、送信スキップ |

### リトライ仕様

- WebhookJob自体にはリトライ設定なし
- エラー時はStandardErrorをキャッチしてログ出力のみ

### エラー時の追加処理

```ruby
# agent_bot_webhookの場合
- 会話がpending状態の場合、open状態に変更
- アクティビティメッセージを作成（"agent_bot.error_moved_to_open"）

# api_inbox_webhookの場合
- メッセージのステータスを'failed'に更新
- エラーメッセージを記録
```

## トランザクション仕様

- イベント処理自体はトランザクション外で実行
- WebhookJobは非同期で実行されるため、元のトランザクションとは独立

## パフォーマンス要件

- イベント受信から配信キュー登録まで: 100ms以内
- Webhook送信タイムアウト: デフォルト5秒（WEBHOOK_TIMEOUT環境変数で設定可能）

## セキュリティ考慮事項

- Webhook URLはHTTP/HTTPSのみ許可（登録時にバリデーション）
- ペイロードにはwebhook_dataメソッドで定義された情報のみ含まれる
- 機密情報（パスワード等）はwebhook_dataに含めない設計

## 備考

- WebhookListenerはSingletonパターンで実装されている
- イベントはWisperを使用してパブリッシュされる
- APIインボックス（Channel::Api）は特別扱いで、自動的にwebhook_urlへ送信される

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

イベントデータとペイロードの構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | base_listener.rb | `app/listeners/base_listener.rb` | イベントからオブジェクトを抽出するヘルパーメソッド |

**読解のコツ**:
- **4-7行目**: `extract_conversation_and_account`で会話とアカウントを抽出
- **17-20行目**: `extract_message_and_account`でメッセージとアカウントを抽出
- **32-37行目**: `extract_changed_attributes`で変更属性を整形

#### Step 2: エントリーポイントを理解する

WebhookListenerの各イベントハンドラを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | webhook_listener.rb | `app/listeners/webhook_listener.rb` | イベントハンドラとペイロード構築 |

**主要処理フロー**:
1. **2-8行目**: `conversation_status_changed`で会話ステータス変更を処理
2. **25-33行目**: `message_created`でメッセージ作成を処理、`webhook_sendable?`チェックあり
3. **54-58行目**: `contact_created`でアカウントWebhookのみに送信
4. **110-116行目**: `deliver_account_webhooks`でサブスクリプションフィルタリング
5. **118-123行目**: `deliver_api_inbox_webhooks`でAPIインボックス判定
6. **125-128行目**: `deliver_webhook_payloads`で両方の配信を統合

#### Step 3: 非同期ジョブを理解する

WebhookJobとWebhooks::Triggerの処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | webhook_job.rb | `app/jobs/webhook_job.rb` | 非同期ジョブの定義 |
| 3-2 | trigger.rb | `lib/webhooks/trigger.rb` | HTTP送信ロジック |

**主要処理フロー**:
- **webhook_job.rb 4-6行目**: `Webhooks::Trigger.execute`を呼び出し
- **trigger.rb 14-19行目**: `execute`メソッドでHTTP送信実行
- **trigger.rb 23-30行目**: `perform_request`でRestClientによるPOST
- **trigger.rb 33-47行目**: `handle_error`でエラー時の追加処理

### プログラム呼び出し階層図

```
[イベント発生]
    │
    ▼
WebhookListener (singleton)
    │
    ├─ #conversation_status_changed
    │      └─ deliver_webhook_payloads
    │
    ├─ #message_created
    │      └─ message.webhook_sendable?
    │      └─ deliver_webhook_payloads
    │
    └─ (その他イベントハンドラ)
           │
           ▼
    deliver_webhook_payloads
           │
           ├─ deliver_account_webhooks
           │      └─ webhook.subscriptions.include?(event)
           │      └─ WebhookJob.perform_later
           │
           └─ deliver_api_inbox_webhooks
                  └─ inbox.channel_type == 'Channel::Api'
                  └─ WebhookJob.perform_later
                         │
                         ▼
                  Webhooks::Trigger.execute
                         │
                         └─ RestClient POST
```

### データフロー図

```
[入力]                          [処理]                              [出力]

Wisperイベント         ───▶ WebhookListener              ───▶ 外部Webhook URL
(conversation,                   │                                │
 message, contact等)             ▼                                │
                         ペイロード構築                             │
                         (webhook_data +                          │
                          event + changed)                        │
                                 │                                │
                                 ▼                                ▼
                         WebhookJob (Sidekiq)         ───▶ HTTP POST
                                 │                          (JSON)
                                 ▼
                         Webhooks::Trigger
                                 │
                                 ▼
                         RestClient.post
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| webhook_listener.rb | `app/listeners/webhook_listener.rb` | リスナー | イベント受信とペイロード構築 |
| base_listener.rb | `app/listeners/base_listener.rb` | 基底クラス | リスナー共通ヘルパー |
| webhook_job.rb | `app/jobs/webhook_job.rb` | ジョブ | 非同期送信ジョブ |
| trigger.rb | `lib/webhooks/trigger.rb` | サービス | HTTP送信ロジック |
| webhook.rb | `app/models/webhook.rb` | モデル | Webhookデータモデル |
| conversation.rb | `app/models/conversation.rb` | モデル | webhook_dataメソッド提供 |
| message.rb | `app/models/message.rb` | モデル | webhook_data, webhook_sendable?メソッド提供 |
| contact.rb | `app/models/contact.rb` | モデル | webhook_dataメソッド提供 |
| event_data_presenter.rb | `app/presenters/inbox/event_data_presenter.rb` | プレゼンター | インボックスのwebhook_data生成 |
