# 機能設計書 43-メッセージ送受信

## 概要

本ドキュメントは、Chatwootにおけるテキスト・メディア・テンプレートメッセージの送受信機能の設計を記述します。

### 本機能の処理概要

会話内でのメッセージ送受信を管理する中核機能です。テキストメッセージ、画像・動画・ファイルなどのメディアメッセージ、WhatsApp等で使用されるテンプレートメッセージなど、多様な形式のメッセージを統一的に処理します。

**業務上の目的・背景**：カスタマーサポートの基本機能として、顧客とエージェント間のコミュニケーションを実現します。複数のチャネル（Web Widget、メール、WhatsApp、Facebook、Telegram等）からのメッセージを統一的に処理し、エージェントは単一のインターフェースで顧客対応を行えます。メッセージの永続化、リアルタイム通知、各チャネルへの配信を担当します。

**機能の利用シーン**：
- エージェントが会話画面から顧客に返信する場面
- 顧客がWeb Widgetやソーシャルメディアからメッセージを送信する場面
- ボットやオートメーションが自動返信を送信する場面
- キャンペーンでテンプレートメッセージを送信する場面

**主要な処理内容**：
1. メッセージの作成と保存
2. 添付ファイルの処理
3. 各チャネルへのメッセージ配信（SendReplyJob）
4. リアルタイム通知（WebSocket）
5. メール通知（必要に応じて）
6. 会話の再オープン（incoming時）

**関連システム・外部連携**：
- 各チャネルAPI（Facebook、WhatsApp、Telegram、Twitter等）
- メール配信システム（Email inbox）
- WebSocket（ActionCable）
- Active Storage（ファイル保存）

**権限による制御**：会話にアクセス可能なエージェントがメッセージを送信できます。APIインボックスの場合のみincomingメッセージの作成が許可されます。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | ダッシュボードホーム画面 | 主画面 | 選択した会話のメッセージ表示・送信 |
| 11 | 会話詳細画面 | 主画面 | メッセージの送受信処理 |
| 19 | インボックスビュー詳細 | 主画面 | メッセージの送受信 |
| 116 | Widget メッセージ画面 | 主画面 | チャットメッセージの送受信 |

## 機能種別

CRUD操作 / データ連携 / リアルタイム通信

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| content | String | No | メッセージ本文 | 最大150,000文字 |
| message_type | String | No | メッセージタイプ | incoming/outgoing/activity/template |
| private | Boolean | No | プライベートノートフラグ | デフォルト: false |
| content_type | String | No | コンテンツタイプ | text/input_text/cards/form/article等 |
| attachments | Array | No | 添付ファイル | 最大15ファイル |
| content_attributes | Hash | No | 追加属性（in_reply_to等） | - |
| template_params | Hash | No | テンプレートパラメータ | name必須（テンプレート時） |
| echo_id | String | No | フロントエンド用一時ID | - |
| source_id | String | No | 外部システムのメッセージID | - |
| cc_emails | String | No | CC宛先（Email） | カンマ区切り |
| bcc_emails | String | No | BCC宛先（Email） | カンマ区切り |

### 入力データソース

- 画面入力：会話詳細画面のメッセージ入力フォーム
- Widget：Webウィジェットからの入力
- 外部API：各チャネルからのWebhook
- API：`POST /api/v1/accounts/{account_id}/conversations/{conversation_id}/messages`

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | Integer | メッセージID |
| content | String | メッセージ本文 |
| message_type | Integer | メッセージタイプ（0:incoming, 1:outgoing, 2:activity, 3:template） |
| content_type | Integer | コンテンツタイプ |
| status | Integer | ステータス（0:sent, 1:delivered, 2:read, 3:failed） |
| private | Boolean | プライベートノートフラグ |
| sender | Object | 送信者情報 |
| attachments | Array | 添付ファイル情報 |
| created_at | DateTime | 作成日時 |
| conversation_id | Integer | 会話ID（display_id） |

### 出力先

- API レスポンス：JSON形式でメッセージ情報を返却
- データベース：messagesテーブル、attachmentsテーブル
- WebSocket：MESSAGE_CREATEDイベント
- 各チャネル：SendReplyJobによる配信
- メール：WebWidget/APIの場合のメール通知

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ MessagesController#createが呼び出される
2. MessageBuilder初期化
   └─ パラメータの解析、送信者の特定
3. メッセージレコード作成
   └─ conversation.messages.buildで新規メッセージ
4. 添付ファイル処理
   └─ process_attachmentsで各添付ファイルを関連付け
5. メール関連処理（Email inbox）
   └─ process_emails, process_email_content
6. 保存処理
   └─ message.save!でデータベースに永続化
7. After Create Commit コールバック
   ├─ reopen_conversation: 会話の再オープン
   ├─ set_conversation_activity: last_activity_at更新
   ├─ dispatch_create_events: MESSAGE_CREATED発火
   ├─ send_reply: SendReplyJobエンキュー
   ├─ execute_message_template_hooks: テンプレートフック
   └─ update_contact_activity: 連絡先活動更新
8. チャネル配信
   └─ SendReplyJobが各チャネルサービスを呼び出し
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B[MessageBuilder初期化]
    B --> C{message_type検証}
    C -->|APIインボックス以外でincoming| D[エラー返却]
    C -->|OK| E[メッセージレコード作成]
    E --> F{添付ファイルあり?}
    F -->|Yes| G[process_attachments]
    F -->|No| H[Emailインボックス?]
    G --> H
    H -->|Yes| I[process_emails/email_content]
    H -->|No| J[save!]
    I --> J
    J --> K[After Create Commit]
    K --> L[reopen_conversation]
    L --> M[dispatch_create_events]
    M --> N[send_reply]
    N --> O{添付ファイルあり?}
    O -->|Yes| P[2秒後にSendReplyJob]
    O -->|No| Q[即時SendReplyJob]
    P --> R[チャネル配信]
    Q --> R
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-43-001 | Incomingメッセージ制限 | APIインボックス以外ではincomingメッセージ作成不可 | message_type=incoming時 |
| BR-43-002 | 添付ファイル上限 | 1メッセージあたり最大15ファイル | 添付ファイル追加時 |
| BR-43-003 | メッセージフラッディング防止 | 1分間に一定数以上のメッセージは作成不可 | メッセージ作成時 |
| BR-43-004 | 会話再オープン | incomingメッセージでsnoozed/resolved会話を再オープン | incoming && !muted |
| BR-43-005 | 初回応答記録 | 条件を満たす最初のoutgoingメッセージでfirst_reply_created_atを設定 | valid_first_reply?時 |
| BR-43-006 | 待機時間更新 | incomingメッセージでwaiting_sinceを設定、応答でクリア | 応答待ち状態管理 |

### 計算ロジック

**初回応答判定（valid_first_reply?）**:
```ruby
outgoing? &&
sender.is_a?(User) &&
!private? &&
conversation.first_reply_created_at.blank? &&
automation_rule_id.blank? &&
campaign_id.blank?
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| メッセージ作成 | messages | INSERT | メッセージの新規作成 |
| 添付ファイル作成 | attachments | INSERT | 添付ファイル情報の作成 |
| 会話更新 | conversations | UPDATE | last_activity_at, status等の更新 |
| 連絡先更新 | contacts | UPDATE | last_activity_atの更新（送信者がContactの場合） |

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

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id | conversation.account_id | - |
| INSERT | inbox_id | conversation.inbox_id | - |
| INSERT | conversation_id | 対象会話のID | - |
| INSERT | message_type | incoming/outgoing/activity/template | enum |
| INSERT | content | メッセージ本文 | 最大150,000文字 |
| INSERT | content_type | text等 | enum |
| INSERT | private | true/false | プライベートノート |
| INSERT | sender_type | User/Contact/AgentBot | ポリモーフィック |
| INSERT | sender_id | 送信者のID | - |
| INSERT | status | sent | 初期値 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | バリデーションエラー | APIインボックス以外でincoming | outgoingで送信 |
| 422 | バリデーションエラー | 添付ファイル上限超過 | ファイル数を削減 |
| 422 | バリデーションエラー | メッセージフラッディング | 送信頻度を下げる |
| 422 | バリデーションエラー | contentが150,000文字超過 | 文字数を削減 |
| 500 | 内部エラー | チャネル配信失敗 | リトライ実行 |

### リトライ仕様

SendReplyJobはSidekiqのデフォルトリトライ設定に従い、失敗時は自動リトライされます。

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

MessageBuilder#performは以下の処理を含みますが、明示的なトランザクションは使用していません：
- メッセージと添付ファイルはautosaveオプションにより一括保存されます
- after_create_commitコールバックはコミット後に実行されるため、ロールバック時は実行されません

## パフォーマンス要件

- メッセージ作成は同期処理、チャネル配信は非同期（SendReplyJob）
- 添付ファイルがある場合は2秒の遅延後にジョブ実行（Active Storage同期待ち）
- messagesテーブルには複数のインデックスが設定されています
- 高度な検索機能有効時はSearchkickで非同期インデックス更新

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

- Strong Parametersによる入力フィルタリング
- ConversationPolicyによる認可チェック
- WebWidget経由の添付ファイルはファイルタイプとサイズを検証
- プライベートノートは顧客には表示されません

## 備考

- echo_idはフロントエンドで一時的なメッセージ表示に使用され、WebSocketで返却されます
- source_idは外部システムとのメッセージ同期に使用されます
- テンプレートメッセージはWhatsApp等の事前承認済みテンプレートを使用します

---

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

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

### 推奨読解順序

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

まず、Messageモデルのスキーマと関連を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | message.rb | `app/models/message.rb` | スキーマ定義（1-39行目）、enum定義（85-101行目）、バリデーション（70-81行目） |
| 1-2 | attachment.rb | `app/models/attachment.rb` | 添付ファイルのスキーマ（1-23行目）、file_type enum（42-43行目） |

**読解のコツ**: Messageモデルのmessage_type（incoming/outgoing/activity/template）とcontent_type（text/input_text/cards等）の違いを理解することが重要です。

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

APIエンドポイントとビルダーの処理を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | messages_controller.rb | `app/controllers/api/v1/accounts/conversations/messages_controller.rb` | createアクション（8-14行目） |
| 2-2 | message_builder.rb | `app/builders/messages/message_builder.rb` | perform処理（23-32行目）、message_params（132-147行目） |

**主要処理フロー**:
1. **8-14行目** (messages_controller.rb): MessageBuilderを初期化してperformを呼び出し
2. **23-32行目** (message_builder.rb): メッセージ作成、添付処理、保存の流れ

#### Step 3: 添付ファイル処理を理解する

添付ファイルの処理フローを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | message_builder.rb | `app/builders/messages/message_builder.rb` | process_attachments（50-67行目） |

**主要処理フロー**:
- **50-67行目**: 添付ファイルごとにattachment.buildでメッセージに関連付け、file_typeを設定

#### Step 4: After Createコールバックを理解する

メッセージ作成後の処理を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | message.rb | `app/models/message.rb` | execute_after_create_commit_callbacks（310-318行目） |

**主要処理フロー**:
- **310-318行目**: reopen_conversation, set_conversation_activity, dispatch_create_events, send_reply, execute_message_template_hooks, update_contact_activityを順次実行

#### Step 5: チャネル配信を理解する

各チャネルへの配信処理を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | send_reply_job.rb | `app/jobs/send_reply_job.rb` | CHANNEL_SERVICES定数（4-16行目）、perform（18-28行目） |

**主要処理フロー**:
- **4-16行目**: チャネルタイプごとのサービスクラスをマッピング
- **18-28行目**: チャネル名に応じたサービスを呼び出し

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

```
MessagesController#create
    │
    └─ Messages::MessageBuilder#perform
           │
           ├─ conversation.messages.build(message_params)
           │
           ├─ process_attachments
           │      └─ message.attachments.build
           │
           ├─ process_emails (Email inbox)
           │
           ├─ process_email_content (Email inbox)
           │
           └─ message.save!
                  │
                  └─ after_create_commit :execute_after_create_commit_callbacks
                         │
                         ├─ reopen_conversation
                         │      └─ conversation.open! / pending!
                         │
                         ├─ set_conversation_activity
                         │      └─ conversation.update_columns(last_activity_at)
                         │
                         ├─ dispatch_create_events
                         │      └─ dispatcher.dispatch(MESSAGE_CREATED)
                         │
                         ├─ send_reply
                         │      └─ SendReplyJob.perform_later
                         │             │
                         │             └─ Channel Service (Twitter/Twilio/Line/Telegram等)
                         │
                         ├─ execute_message_template_hooks
                         │
                         └─ update_contact_activity
```

### データフロー図

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

content/attachments ───▶ MessagesController#create      ───▶ DB (messages, attachments)
                              │
                              ▼
                    Messages::MessageBuilder
                              │
                              ├─▶ after_create_commit     ───▶ WebSocket (MESSAGE_CREATED)
                              │
                              └─▶ SendReplyJob            ───▶ 各チャネルAPI
                                       │                       (Twitter/WhatsApp/
                                       │                        Telegram/Email等)
                                       │
                                       └────────────────────▶ Email通知
                                                              (WebWidget/API)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| message.rb | `app/models/message.rb` | モデル | メッセージデータモデル |
| attachment.rb | `app/models/attachment.rb` | モデル | 添付ファイルモデル |
| messages_controller.rb | `app/controllers/api/v1/accounts/conversations/messages_controller.rb` | コントローラー | メッセージAPI |
| message_builder.rb | `app/builders/messages/message_builder.rb` | ビルダー | メッセージ作成ロジック |
| send_reply_job.rb | `app/jobs/send_reply_job.rb` | ジョブ | チャネル配信ジョブ |
| _message.json.jbuilder | `app/views/api/v1/models/_message.json.jbuilder` | ビュー | APIレスポンス生成 |
| message_finder.rb | `app/finders/message_finder.rb` | ファインダー | メッセージ検索 |
