# 通知設計書 5-conversation_mention

## 概要

本ドキュメントは、Chatwootシステムにおける「conversation_mention（メンション通知）」の設計仕様を記載したものである。会話内のプライベートメッセージでユーザーまたはチームがメンションされた際に、該当ユーザーに通知を送信する機能の詳細を定義する。

### 本通知の処理概要

本通知は、プライベートノート（内部メモ）内でエージェントがメンションされた際に、そのエージェントに即座に通知することで、チーム間のコラボレーションと迅速な情報共有を実現するための機能である。

**業務上の目的・背景**：カスタマーサポート業務において、複雑な問い合わせや専門知識が必要なケースでは、他のエージェントの協力が必要になることがある。本通知により、メンションされたエージェントは自身が呼ばれていることを即座に認識し、該当の会話に参加して助言やサポートを提供できる。また、チームメンションにより、チーム全体に一度に通知を送ることも可能であり、効率的なエスカレーションやチーム協業を実現する。

**通知の送信タイミング**：プライベートメッセージ（private: true）が作成され、`MESSAGE_CREATED`イベントがディスパッチされた後、`Messages::MentionService`が呼び出された時点で送信される。メッセージ内容から`(mention://user/{id}/{name})`または`(mention://team/{id}/{name})`形式のメンションを検出して処理する。

**通知の受信者**：メンションされたユーザーが受信者となる。チームがメンションされた場合は、そのチームの全メンバーに展開される。ただし、受信トレイのメンバーまたはアカウント管理者のみが有効な受信者となる。

**通知内容の概要**：メンションされた会話のID、メンションを含むメッセージの内容、最近のメッセージ履歴、および会話へのリンクが含まれる。

**期待されるアクション**：メンションされたエージェントは通知を受け取った後、会話詳細ページにアクセスし、プライベートノートで返信するか、または直接顧客への対応に参加することが期待される。

## 通知種別

- メール通知
- プッシュ通知（ブラウザプッシュ、FCMモバイルプッシュ）
- アプリ内通知（リアルタイム）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（ActiveJob） |
| 優先度 | 中（defaultキュー） |
| リトライ | Sidekiqデフォルト設定に準拠 |

### 送信先決定ロジック

1. メッセージがプライベートかつ内容が存在し、メンションが含まれることを確認
2. メッセージ内容からユーザーメンション（`mention://user/{id}/{name}`）を抽出
3. メッセージ内容からチームメンション（`mention://team/{id}/{name}`）を抽出
4. チームメンションを該当チームのメンバーIDリストに展開
5. ユーザーIDとチームメンバーIDを統合し、重複を排除
6. 受信トレイのメンバーまたはアカウント管理者のみにフィルタリング
7. 各対象ユーザーに対して`NotificationBuilder`で通知を作成
8. **特例**: コンタクトがブロックされている場合でも、メンション通知は送信される

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | 環境変数`MAILER_SENDER_EMAIL`（デフォルト: accounts@chatwoot.com） |
| 送信元名称 | Chatwoot（環境変数で設定可能） |
| 件名 | `{agent_name}, You have been mentioned in conversation [ID - {display_id}]` |
| 形式 | HTML（Liquidテンプレート） |

### 本文テンプレート

```html
<p>Hi {{user.available_name}}, </p>

<p>You've been mentioned in a conversation. <b>{{message.sender_display_name}}</b> wrote:</p>
<blockquote style="box-sizing:border-box; padding:8px 16px; margin:0 0 20px; font-size:14px; border-left:5px solid #eeeeee">
  {{message.text_content}}
</blockquote>

<p><b>Previous messages:</b></p>
{% for chat_message in conversation.recent_messages %}
  <div>
    <h4 style="margin: 0;">
      {% if chat_message.sender == user.available_name %}
        You
      {% else %}
        {{chat_message.sender}}
      {% endif %}
    </h4>
  </div>

  <div style="padding: 10px 20px; margin: 5px 0 20px 0; background: #F2F3F7; border-radius: 10px;">
    {% if chat_message.content %}
      {{chat_message.content}}
    {% endif %}

    {% if chat_message.attachments %}
      {% for attachment in chat_message.attachments %}
        Attachment [<a href="{{ attachment }}" _target="blank">Click here to view</a>]
      {% endfor %}
    {% endif %}
  </div>
{% endfor %}
<p>
  <a href="{{ action_url }}">View Message</a>
</p>
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | メール本文に添付ファイルへのリンクのみ含まれる |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| user.available_name | エージェントの表示名 | users.available_name | Yes |
| conversation.display_id | 会話の表示ID | conversations.display_id | Yes |
| message.sender_display_name | メンションを含むメッセージの送信者名 | messages.sender.name | Yes |
| message.text_content | メンションを含むメッセージの内容 | messages.content | Yes |
| conversation.recent_messages | 最新5件のメッセージ | messages (chat, last 5) | Yes |
| action_url | 会話詳細ページURL | 動的生成 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| イベント | MESSAGE_CREATED | プライベートメッセージ かつ メンション含む | プライベートノート作成時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| メッセージがプライベートでない | private: falseの場合 |
| メッセージ内容が空 | content.blank?の場合 |
| メンションが含まれない | ユーザーもチームもメンションされていない場合 |
| メンションされたユーザーが受信トレイメンバーでも管理者でもない | 有効な対象者がいない場合 |
| メール通知時、既読の場合 | notification.read_atが設定済みの場合はメール送信しない |
| ユーザーがメール未確認 | user.confirmed_atがnilの場合はメール送信しない |

**特例**: コンタクトがブロックされている場合でも、メンション通知は例外的に送信される。

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[MESSAGE_CREATEDイベント発生] --> B{プライベートメッセージ?}
    B -->|No| C[通知スキップ]
    B -->|Yes| D{内容が存在?}
    D -->|No| C
    D -->|Yes| E{メンションが含まれる?}
    E -->|No| C
    E -->|Yes| F[ユーザーメンション抽出]
    F --> G[チームメンション抽出]
    G --> H[チームをメンバーIDに展開]
    H --> I[IDリストを統合・重複排除]
    I --> J[有効なメンション対象者にフィルタ]
    J --> K{有効な対象者あり?}
    K -->|No| C
    K -->|Yes| L[UserMentionJob実行]
    L --> M[各対象者にループ]
    M --> N[NotificationBuilder実行]
    N --> O[会話参加者として追加]
    O --> P{次の対象者あり?}
    P -->|Yes| M
    P -->|No| Q[完了]
    C --> Q
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| messages | メッセージ情報取得 | secondary_actor、メンション解析 |
| conversations | 会話情報取得 | primary_actor |
| users | エージェント情報 | メンション対象者 |
| teams | チーム情報 | チームメンション展開 |
| team_members | チームメンバー情報 | チームメンション展開 |
| inbox_members | 受信トレイメンバー情報 | フィルタリング |
| account_users | アカウント管理者情報 | フィルタリング |
| notification_settings | 通知設定 | 購読状況確認 |

### テーブル別参照項目詳細

#### messages

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | メッセージ特定 | イベントデータから |
| content | メンション解析 | 正規表現でパース |
| private | プライベート判定 | true/false |
| conversation_id | 会話特定 | FK |
| account_id | アカウント特定 | FK |
| inbox_id | 受信トレイ特定 | FK |

#### teams

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | チーム特定 | メンションIDから |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| notifications | INSERT | 通知レコード作成 |
| conversation_participants | INSERT | メンションされたユーザーを参加者として追加 |

#### 送信ログテーブル（notifications）

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | notification_type | 4 (conversation_mention) | ENUM値 |
| INSERT | account_id | 会話のaccount_id | FK |
| INSERT | user_id | メンションされたユーザーID | FK |
| INSERT | primary_actor_type | 'Conversation' | ポリモーフィック |
| INSERT | primary_actor_id | 会話ID | FK |
| INSERT | secondary_actor_type | 'Message' | メッセージ |
| INSERT | secondary_actor_id | メッセージID | FK |
| INSERT | last_activity_at | created_at | 作成時刻 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗（SMTP） | SMTPサーバー接続エラー | ログ出力、例外キャッチ |
| WebPush期限切れ | WebPush::ExpiredSubscription | サブスクリプション削除 |
| WebPush無効 | WebPush::InvalidSubscription | サブスクリプション削除 |
| WebPush認証エラー | WebPush::Unauthorized | サブスクリプション削除 |
| WebPushレート制限 | WebPush::TooManyRequests | ログ警告出力 |
| FCMエラー | レスポンスにerror含む | サブスクリプション削除 |
| ユーザー不存在 | User.find(user_id)で見つからない | ActiveRecord::RecordNotFound |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqデフォルト（25回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | 一時的なネットワークエラー等 |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（システムレベル） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし。24時間送信可能。

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

- メール送信はSMTP over TLS/SSLを推奨
- プッシュ通知はVAPID認証を使用
- FCM通知はFirebase認証を使用
- 通知内容に個人情報（メッセージ内容）が含まれるため、HTTPS必須
- ユーザー確認済み（confirmed_at）のチェックにより、未確認アカウントへの送信を防止
- メンション対象者は受信トレイメンバーまたはアカウント管理者に制限

## 備考

- メンション通知は、ブロックされたコンタクトの会話でも例外的に送信される
- プライベートメッセージ内でのみメンションが有効
- チームメンションは該当チームの全メンバーに展開される
- メンションされたユーザーは自動的に会話参加者として追加される
- `Conversations::UserMentionJob`でメンションイベントも別途発火される
- メンション通知が先に送信された場合、後続の通知（担当者通知、参加者通知）は重複を避けるためスキップされる

---

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

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

### 推奨読解順序

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

通知システムの中核となるデータ構造を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notification.rb | `app/models/notification.rb` | NOTIFICATION_TYPESの定義（42行目: conversation_mention: 4） |

**読解のコツ**: メンション通知はnotification_type=4として定義されている。

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

処理の起点となるリスナーを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | notification_listener.rb | `app/listeners/notification_listener.rb` | message_created（50-55行目） |

**主要処理フロー**:
1. **53行目**: Messages::MentionService.new(message: message).performが呼び出される
2. メンション通知は新規メッセージ通知より先に処理される

#### Step 3: メンションサービスを理解する

メンション解析と通知作成のロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | mention_service.rb | `app/services/messages/mention_service.rb` | 全体（1-68行目） |

**主要処理フロー**:
- **5行目**: valid_mention_message?でプライベートメッセージかつ内容・メンションがあることを確認
- **7行目**: filter_mentioned_ids_by_inboxで有効なメンション対象者にフィルタ
- **10行目**: Conversations::UserMentionJobを実行
- **11行目**: generate_notifications_for_mentionsで通知を生成
- **12行目**: add_mentioned_users_as_participantsで会話参加者として追加

#### Step 4: メンション解析ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | mention_service.rb | `app/services/messages/mention_service.rb` | mentioned_ids（21-28行目）、expand_team_mentions_to_users（30-38行目） |

**主要処理フロー**:
- **22行目**: `mention://user/{id}/{name}`形式のユーザーメンションを正規表現で抽出
- **23行目**: `mention://team/{id}/{name}`形式のチームメンションを正規表現で抽出
- **25行目**: expand_team_mentions_to_usersでチームをメンバーIDに展開
- **27行目**: ユーザーIDとチームメンバーIDを統合し、uniqで重複排除

#### Step 5: フィルタリングロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | mention_service.rb | `app/services/messages/mention_service.rb` | valid_mentionable_user_ids（40-45行目）、filter_mentioned_ids_by_inbox（47-49行目） |

**主要処理フロー**:
- **42-43行目**: 受信トレイのメンバーIDと管理者IDを取得
- **48行目**: mentioned_idsとvalid_mentionable_user_idsの積集合を取得

#### Step 6: 通知作成ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 6-1 | mention_service.rb | `app/services/messages/mention_service.rb` | generate_notifications_for_mentions（51-60行目） |
| 6-2 | notification_builder.rb | `app/builders/notification_builder.rb` | build_notification（25-38行目）、特にブロック例外（29行目） |

**主要処理フロー**:
- **52行目**: validated_mentioned_ids.eachでループ
- **53-59行目**: NotificationBuilderで通知作成（secondary_actorにメッセージを設定）
- **29行目**: `notification_type != 'conversation_mention'`でブロック例外を処理

#### Step 7: メール通知を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 7-1 | conversation_notifications_mailer.rb | `app/mailers/agent_notifications/conversation_notifications_mailer.rb` | conversation_mention（23-32行目） |
| 7-2 | conversation_mention.liquid | `app/views/mailers/agent_notifications/conversation_notifications_mailer/conversation_mention.liquid` | メールテンプレート全体 |

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

```
Message#after_create_commit (dispatch_create_events)
    |
    +-- dispatcher_dispatch(MESSAGE_CREATED)
           |
           +-- NotificationListener#message_created
                  |
                  +-- Messages::MentionService#perform
                         |
                         +-- valid_mention_message? (プライベート・内容・メンション確認)
                         |
                         +-- mentioned_ids (ユーザー・チームメンション抽出)
                         |      |
                         |      +-- expand_team_mentions_to_users (チーム展開)
                         |
                         +-- filter_mentioned_ids_by_inbox (有効対象者フィルタ)
                         |      |
                         |      +-- valid_mentionable_user_ids
                         |             (受信トレイメンバー + 管理者)
                         |
                         +-- Conversations::UserMentionJob.perform_later
                         |
                         +-- generate_notifications_for_mentions
                         |      |
                         |      +-- validated_mentioned_ids.each
                         |             |
                         |             +-- NotificationBuilder#perform
                         |                    |
                         |                    +-- build_notification
                         |                           (ブロック例外: conversation_mention)
                         |
                         +-- add_mentioned_users_as_participants
                                |
                                +-- conversation_participants.find_or_create_by
```

### データフロー図

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

Message                    MentionService                       Notification
(プライベート)    ───────▶  (メンション解析)         ───────▶    (DB保存)
       │                           │
       │                           ▼
       │                   mentioned_ids
       │                   (正規表現でID抽出)
       │                           │
       │              ┌────────────┴────────────┐
       │              ▼                         ▼
       │     ユーザーメンション           チームメンション
       │     mention://user/...         mention://team/...
       │              │                         │
       │              │                         ▼
       │              │              expand_team_mentions_to_users
       │              │              (チーム→メンバーID展開)
       │              │                         │
       │              └────────────┬────────────┘
       │                           ▼
       │                   filter_mentioned_ids_by_inbox
       │                   (有効対象者のみ抽出)
       │                           │
       ▼                           ▼
 secondary_actor           generate_notifications_for_mentions
 (メッセージ参照)           (通知作成ループ)
                                   │
                                   ▼
                           add_mentioned_users_as_participants
                           (参加者として追加)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification.rb | `app/models/notification.rb` | ソース | 通知モデル・配信トリガー |
| notification_listener.rb | `app/listeners/notification_listener.rb` | ソース | イベントリスナー |
| mention_service.rb | `app/services/messages/mention_service.rb` | ソース | メンションサービス（中核） |
| notification_builder.rb | `app/builders/notification_builder.rb` | ソース | 通知ビルダー（ブロック例外） |
| conversation_notifications_mailer.rb | `app/mailers/agent_notifications/conversation_notifications_mailer.rb` | ソース | メーラー |
| conversation_mention.liquid | `app/views/mailers/agent_notifications/conversation_notifications_mailer/conversation_mention.liquid` | テンプレート | メールテンプレート |
| user_mention_job.rb | `app/jobs/conversations/user_mention_job.rb` | ソース | メンションイベントジョブ |
| message.rb | `app/models/message.rb` | ソース | メッセージモデル |
| team.rb | `app/models/team.rb` | ソース | チームモデル |
| team_member.rb | `app/models/team_member.rb` | ソース | チームメンバーモデル |
