# 機能設計書 145-Slack統合

## 概要

本ドキュメントは、ChatwootとSlackを連携する統合機能の設計書である。OAuth2認証によりSlackワークスペースと接続し、Chatwootの会話をSlackチャネルに同期することで、チーム内でのコミュニケーションを効率化する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポートチームは、多くの場合Slackを社内コミュニケーションツールとして使用している。Chatwoot上の顧客との会話をSlackチャネルにリアルタイムで同期することで、サポートメンバー間での情報共有が容易になり、迅速な対応が可能になる。また、Slackからの返信をChatwootに反映させることで、チャネル間のコミュニケーションギャップを解消する。

**機能の利用シーン**：
- 新しい会話が作成された際にSlackチャネルに通知
- 顧客からのメッセージをSlackスレッドに転送
- エージェントの返信をSlackに同期
- Slackからの返信をChatwoot会話に反映
- ファイル添付の共有

**主要な処理内容**：
1. Slack OAuth2認証によるワークスペース接続
2. Slackチャネルの一覧取得と選択
3. 会話とSlackスレッドの紐付け
4. メッセージの双方向同期
5. 添付ファイルのアップロード
6. リンクアンフォール（展開）

**関連システム・外部連携**：
- Slack Web API（OAuth2、メッセージ送信、ファイルアップロード）
- Slack Events API（受信メッセージ）

**権限による制御**：
- Slack統合の設定は管理者権限が必要
- Slack OAuth認証に必要なスコープ: commands, chat:write, channels:read, channels:manage, channels:join, groups:read, groups:write, im:write, mpim:write, users:read, users:read.email, chat:write.customize, channels:history, groups:history, mpim:history, im:history, files:read, files:write

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 68 | Slack連携画面 | 主画面 | Slack統合の設定・チャネル選択 |
| 65 | 連携アプリ一覧画面 | 参照画面 | Slack統合の有効化状態表示 |

## 機能種別

データ連携 / OAuth認証 / メッセージ同期

## 入力仕様

### 入力パラメータ（認証）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| code | String | Yes | SlackのOAuth認証コード | Slackから取得したコード |
| inbox_id | Integer | No | インボックスID | 存在するインボックス |

### 入力パラメータ（チャネル設定）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| reference_id | String | Yes | Slackチャネルの識別子 | 有効なSlackチャネルID |

### 入力データソース

- Slack OAuth認証コールバック
- 管理画面のチャネル選択フォーム

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| hook.id | Integer | HookのID |
| hook.app_id | String | 'slack' |
| hook.access_token | String | Slackアクセストークン（暗号化） |
| hook.reference_id | String | 選択されたSlackチャネルID |
| hook.settings | JSONB | チャネル名等の設定 |
| hook.status | String | enabled/disabled |

### 出力先

- integrations_hooksテーブル
- Slackワークスペース（メッセージ）
- conversationsテーブル（identifier更新）
- messagesテーブル（external_source_id_slack更新）

## 処理フロー

### 処理シーケンス（認証フロー）

```
1. 管理者がSlack連携を開始
   └─ /app/accounts/{id}/settings/integrations/slack へアクセス

2. Slack OAuth認証画面へリダイレクト
   └─ https://slack.com/oauth/v2/authorize?scope=...&client_id=...&redirect_uri=...

3. ユーザーが認証を承認

4. コールバック受信
   └─ SlackController#create
   └─ Integrations::Slack::HookBuilder.new.perform

5. アクセストークン取得
   └─ client.oauth_v2_access
   └─ Hook作成（status: 'disabled'）

6. チャネル一覧取得
   └─ Integrations::Slack::ChannelBuilder.fetch_channels

7. チャネル選択・保存
   └─ SlackController#update
   └─ reference_id更新、status: 'enabled'
```

### 処理シーケンス（メッセージ送信）

```
1. メッセージ作成イベント発生
   └─ HookListener#message_created → HookJob

2. Slack送信ジョブ実行
   └─ SendOnSlackJob.perform_later(message, hook)

3. メッセージ送信
   └─ Integrations::Slack::SendOnSlackService.perform
   └─ slack_client.chat_postMessage

4. 添付ファイルアップロード（存在する場合）
   └─ slack_client.files_upload_v2

5. 会話識別子更新
   └─ conversation.update!(identifier: slack_message.ts)

6. メッセージ識別子更新
   └─ message.update!(external_source_id_slack: ...)
```

### フローチャート

```mermaid
flowchart TD
    A[管理者がSlack連携開始] --> B[OAuth認証画面へリダイレクト]
    B --> C[ユーザーが承認]
    C --> D[コールバック受信]
    D --> E[アクセストークン取得]
    E --> F[Hook作成 status=disabled]
    F --> G[チャネル一覧取得]
    G --> H[管理者がチャネル選択]
    H --> I{パブリックチャネル?}
    I -->|Yes| J[conversations_join実行]
    I -->|No| K[スキップ]
    J --> L[Hook更新 status=enabled]
    K --> L
    L --> M[Slack統合完了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-145-01 | 初期ステータス | Slack Hook作成時はdisabled状態 | Hook作成時 |
| BR-145-02 | チャネル選択必須 | チャネル選択後にenabledに変更 | チャネル更新時 |
| BR-145-03 | パブリックチャネル参加 | パブリックチャネル選択時は自動で参加 | チャネル更新時 |
| BR-145-04 | スレッド連携 | 会話のidentifierにSlackスレッドtsを保存 | 初回メッセージ送信時 |
| BR-145-05 | Twitterツイート除外 | Twitterツイートタイプの会話はSlack送信しない | メッセージ送信時 |
| BR-145-06 | 送信元ループ防止 | Slack起源のメッセージは再送信しない | メッセージ送信時 |
| BR-145-07 | プライベートノート表示 | プライベートメッセージは"private:"プレフィックス付き | メッセージ送信時 |
| BR-145-08 | アクティビティ表示 | アクティビティメッセージはイタリック表示 | メッセージ送信時 |

### 計算ロジック

特になし

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 認証 | integrations_hooks | INSERT | Slack Hook作成 |
| チャネル設定 | integrations_hooks | UPDATE | reference_id, status更新 |
| メッセージ送信 | conversations | UPDATE | identifier（Slackスレッドts）更新 |
| メッセージ送信 | messages | UPDATE | external_source_id_slack更新 |

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

#### integrations_hooks

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | app_id | 'slack' | 固定値 |
| INSERT | access_token | OAuth取得トークン | 暗号化保存 |
| INSERT | status | 'disabled' | 初期値 |
| UPDATE | reference_id | SlackチャネルID | チャネル選択時 |
| UPDATE | settings | { channel_name: ... } | チャネル名保存 |
| UPDATE | status | 'enabled' | チャネル選択後 |

#### conversations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | identifier | Slackメッセージのts値 | 初回送信時 |

#### messages

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | external_source_id_slack | "cw-origin-{ts}" | メッセージ送信後 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | AccountInactive | Slackアカウント無効 | Hookを無効化、再認証プロンプト |
| - | MissingScope | スコープ不足 | Hookを無効化、再認証プロンプト |
| - | InvalidAuth | 認証エラー | Hookを無効化、再認証プロンプト |
| - | ChannelNotFound | チャネルが存在しない | Hookを無効化、再認証プロンプト |
| - | NotInChannel | チャネルに参加していない | Hookを無効化、再認証プロンプト |
| 422 | invalid_channel_id | チャネルID不正 | 正しいチャネルを選択 |

### リトライ仕様

```ruby
# SendOnSlackJob
retry_on LockAcquisitionError, wait: 1.second, attempts: 8
```

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

- メッセージ送信はMutexロックで排他制御
- Slack API呼び出しはトランザクション外

## パフォーマンス要件

- チャネル一覧取得: ページネーション対応（limit: 1000）
- メッセージ送信: 添付ファイルありは2秒遅延

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

- access_tokenは暗号化して保存
- OAuth認証時のスコープは必要最小限
- Slackエラー時は自動でHook無効化し再認証を促す
- prompt_reauthorization!で再認証フローを開始

## 備考

- Slack APIはslack-ruby-client gemを使用
- ファイルアップロードはfiles_upload_v2（新API）を使用
- リンクアンフォールはlink_unfurlメソッドで対応

---

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

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

### 推奨読解順序

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

Slack統合の設定構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | apps.yml | `config/integration/apps.yml` | Slackアプリの設定（72-78行目） |
| 1-2 | hook.rb | `app/models/integrations/hook.rb` | slack?メソッド（51-53行目） |

**読解のコツ**:
- **apps.yml 72-78行目**: Slackのapp_id, action（OAuthスコープ）、hook_typeを確認
- **hook.rb 51-53行目**: `slack?`メソッドでSlack Hookかどうかを判定

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

Slack統合コントローラーを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | slack_controller.rb | `app/controllers/api/v1/accounts/integrations/slack_controller.rb` | CRUD操作 |

**主要処理フロー**:
1. **5-7行目**: `list_all_channels`でSlackチャネル一覧取得
2. **9-16行目**: `create`でOAuth認証コードからHook作成
3. **18-21行目**: `update`でチャネル選択・保存
4. **23-26行目**: `destroy`でHook削除

#### Step 3: 認証・チャネル設定を理解する

Hook作成とチャネル設定のサービスを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | hook_builder.rb | `lib/integrations/slack/hook_builder.rb` | Hook作成ロジック |
| 3-2 | channel_builder.rb | `lib/integrations/slack/channel_builder.rb` | チャネル取得・設定 |

**主要処理フロー**:
- **hook_builder.rb 8-19行目**: `perform`でOAuthトークン取得、Hook作成
- **hook_builder.rb 32-41行目**: `fetch_access_token`でSlack APIからトークン取得
- **channel_builder.rb 8-10行目**: `fetch_channels`でチャネル一覧取得
- **channel_builder.rb 43-56行目**: `fetch_channels_by_type`でページネーション対応
- **channel_builder.rb 62-69行目**: `update_reference_id`でチャネル設定保存

#### Step 4: メッセージ送信を理解する

Slackへのメッセージ送信処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | send_on_slack_service.rb | `lib/integrations/slack/send_on_slack_service.rb` | メッセージ送信 |
| 4-2 | send_on_slack_job.rb | `app/jobs/send_on_slack_job.rb` | 送信ジョブ |

**主要処理フロー**:
- **send_on_slack_service.rb 5-14行目**: `perform`でバリデーションと送信
- **send_on_slack_service.rb 102-121行目**: `send_message`でメッセージ投稿とファイルアップロード
- **send_on_slack_service.rb 112-121行目**: `post_message`でchat_postMessage呼び出し
- **send_on_slack_service.rb 123-140行目**: `upload_files`でfiles_upload_v2呼び出し

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

```
SlackController
    │
    ├─ #create
    │      └─ Integrations::Slack::HookBuilder.perform
    │             └─ Slack::Web::Client.oauth_v2_access
    │             └─ account.hooks.new(status: 'disabled')
    │
    ├─ #list_all_channels
    │      └─ Integrations::Slack::ChannelBuilder.fetch_channels
    │             └─ slack_client.conversations_list
    │
    ├─ #update
    │      └─ Integrations::Slack::ChannelBuilder.update
    │             ├─ slack_client.conversations_join (if public)
    │             └─ hook.update!(reference_id, status: 'enabled')
    │
    └─ #destroy
           └─ hook.destroy!

[メッセージ送信フロー]
HookListener#message_created
    │
    └─ HookJob.perform_later
           └─ SendOnSlackJob.perform_later
                  └─ SendOnSlackService.perform
                         ├─ slack_client.chat_postMessage
                         ├─ slack_client.files_upload_v2
                         ├─ conversation.update!(identifier: ts)
                         └─ message.update!(external_source_id_slack: ...)
```

### データフロー図

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

OAuth認証コード        ───▶ HookBuilder              ───▶ integrations_hooks
                               │                              (access_token)
                               ▼
Slackチャネル選択      ───▶ ChannelBuilder           ───▶ integrations_hooks
                               │                              (reference_id)
                               ▼
メッセージ作成         ───▶ SendOnSlackService       ───▶ Slackチャネル
イベント                       │                              (メッセージ)
                               ▼
                       conversations更新              ───▶ conversations
                       (identifier = ts)                   (identifier)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| slack_controller.rb | `app/controllers/api/v1/accounts/integrations/slack_controller.rb` | コントローラー | Slack統合API |
| hook_builder.rb | `lib/integrations/slack/hook_builder.rb` | サービス | OAuth認証・Hook作成 |
| channel_builder.rb | `lib/integrations/slack/channel_builder.rb` | サービス | チャネル取得・設定 |
| send_on_slack_service.rb | `lib/integrations/slack/send_on_slack_service.rb` | サービス | メッセージ送信 |
| send_on_slack_job.rb | `app/jobs/send_on_slack_job.rb` | ジョブ | 送信ジョブ |
| incoming_message_builder.rb | `lib/integrations/slack/incoming_message_builder.rb` | サービス | Slack→Chatwootメッセージ変換 |
| slack_link_unfurl_service.rb | `lib/integrations/slack/slack_link_unfurl_service.rb` | サービス | リンクアンフォール |
| link_unfurl_formatter.rb | `lib/integrations/slack/link_unfurl_formatter.rb` | サービス | アンフォールフォーマット |
| slack_message_helper.rb | `lib/integrations/slack/slack_message_helper.rb` | ヘルパー | メッセージ処理共通 |
| apps.yml | `config/integration/apps.yml` | 設定 | Slackアプリ設定 |
