新型質問箱サービスと Discord の連携

# はじめに
この記事は [HowTelevision Advent Calendar 2024](https://qiita.com/advent-calendar/2024/howtv) 10日目の記事になります。

私は新型質問箱サービス「[mond](https://mond.how/ja/my/requested-topics)」の開発に携わっています。

mond では月額のサブスクとして、メンバー限定の回答を閲覧できるメンバーシップという機能を提供しています。
mond の回答者の中には、メンバー限定のコンテンツを Discord サーバーで展開している方々がいらっしゃいます。そのため、mond のメンバーシップ加入者限定でその Discord サーバーに参加できる権利を付与できたらいいよね、という話になりました。
最近この機能の実装に取り組んだので、本記事ではこの機能に関する設計についてまとめていこうと思います。

# DB 設計
mond のメンバーシップ情報を格納するテーブルなど一部省略していますが、Discord 連携に必要なのはざっと下記のテーブルたちです。

|テーブル名  |概要  |
|---|---|
|`user`  |mond のユーザー情報  |
|`discord_user`  |メンバーシップ参加者の Discord ユーザー情報  |
|`discord_guild`  |メンバーシップ開設者の Discord サーバー情報 |
|`discord_guild_member`  |Discord サーバーへの参加情報を格納する中間テーブル |

Discord サーバーのデータを保持するテーブル名は下記を参考にして `discord_guild` としています。

https://scrapbox.io/discordjs-japan/Discord_API%E3%81%A7%E3%81%AF%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%AE%E3%81%93%E3%81%A8%E3%82%92Guild%E3%81%A8%E5%91%BC%E3%81%B6

> Discord内では、いわゆるグループのことをサーバーと呼ぶが、APIを使ってボットを作るときはGuildと呼ぶことが多い

```mermaid
erDiagram
	user {
		string id PK	
	}
	discord_user {
		string id PK "ex. '1234567890'"
		string username "ex. 'test_man'"
		string access_token
		timestamp access_token_expires_at
		string refresh_token
		timestamp created_at
		timestamp updated_at
		string user_id FK "UNIQUE 制約"
	}
	discord_guild {
		string id PK "ギルド ID"
		string name "ギルド名"
		string access_token
		timestamp access_token_expires_at
		string refresh_token
		timestamp created_at
		timestamp updated_at
		string owner_id FK "UNIQUE 制約"
	}
	discord_guild_member {
		string guild_id PK
		string discord_user_id PK
		timestamp joined_at
	}
	
	user ||--o| discord_user: "メンバーシップ参加者の Discord ユーザー情報"
	user ||--o| discord_guild: "メンバーシップ開設者の Discord サーバー情報"
	discord_user ||--o{ discord_guild_member: ""
	discord_guild ||--o{ discord_guild_member: ""
```

# メンバーシップ開設者側
まずは、メンバーシップ開設者側の Dicord 連携です。メンバーシップ開設者が保有する Discord サーバーと mond を連携させるための処理になります。

## 全体像
```mermaid
sequenceDiagram
    actor O as メンシプ開設者
    participant F as mond FE
    participant B as mond BE
    participant D as Discord API

    %% Discord 連携開始
    O->>F: Discord 連携ボタン押下<br/>GET /oauth2/authorize
    Note right of O: client_id, redirect_uri,<br/>scope, permissions, response_type
    F->>O: Discord 認可画面へリダイレクト

    %% OAuth 認可フロー＆Bot の追加
    O->>D: ユーザーが認可＆サーバー選択
    Note right of O: この時点で選択したサーバーに<br/>Bot も同時に追加される
    D->>B: リダイレクト with 認可コード＆guild_id

    %% トークン取得
    B->>D: POST /oauth2/token
    Note right of B: client_id, client_secret,<br/>code, redirect_uri, grant_type
    D->>B: アクセストークン・リフレッシュトークン等返却
    
    %% Discord 情報取得
    B->>D: GET /users/@me/guilds
    D->>B: Discord ギルド一覧

    %% DB 保存
    B->>B: discord_guild テーブルに保存

    %% 完了通知
    B->>F: 成功レスポンス
    F->>O: 完了トースト表示
```

## アクセストークンに必要なスコープ
| スコープ名 | 説明 | 認可画面に表示される文言 |
|------------|------|------------------------|
| `identify` | allows /users/@me without email | ユーザー名、アバター画像、バナーにアクセス |
| `guilds` | allows /users/@me/guilds to return basic information about all of a user's guilds | あなたがどのサーバーに入っているか確認します |
| `bot` | for oauth2 bots, this puts the bot in the user's selected guild by default | Bot をサーバーに追加 |

## Bot に与える必要がある権限
| 権限名 | 説明 | 認可画面に表示される文言 |
|--------|------|------------------------|
| `CREATE_INSTANT_INVITE` | Allows creation of instant invites | 招待を作成 |
| `KICK_MEMBERS` | Allows kicking members | メンバーをキック |  
| `MANAGE_CHANNELS` | Allows management and editing of channels | チャンネルの管理 |
| `MANAGE_GUILD` | Allows management and editing of the guild | サーバー管理 |
| `MANAGE_ROLES` | Allows management and editing of roles | ロールの管理 |
| `MANAGE_WEBHOOKS` | Allows management and editing of webhooks | ウェブフックの管理 |

## 実際の流れ
|1. mond の設定画面から連携を開始する|2. Discord サーバー情報へのアクセスを mond に許可する|3. mond の Bot に与える権限を許可する|4. Bot が Discord サーバーに追加される|
|---|---|---|---|
|![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/4ad09a46-7666-4cb9-1117-0a1081907302.png)|![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/4f288a23-c152-ec58-2086-4b0e5bb06e8a.png)|![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/816347c1-fe44-a9b4-3134-81196d76318a.png)|![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/fc38fe59-61d6-815a-4681-b2cc856ff53e.png)

# メンバーシップ参加者側
次に、メンバーシップ参加者側の Dicord 連携です。こちらはメンバーシップ参加者の Discord アカウントと mond を連携させるための処理になります。

## 全体像
```mermaid
sequenceDiagram
    actor U as メンシプ参加者
    participant F as mond FE
    participant B as mond BE
    participant D as Discord API
    participant Bot as Discord Bot

    %% Discord 連携開始
    U->>F: Discord 連携ボタン押下<br/>GET /oauth2/authorize
    Note right of U: client_id, redirect_uri,<br/>scope, response_type
    F->>U: Discord 認可画面へリダイレクト

    %% OAuth 認可フロー
    U->>D: ユーザーが認可
    D->>B: リダイレクト with 認可コード
    B->>D: POST /oauth2/token
    Note right of B: client_id, client_secret,<br/>code, redirect_uri, grant_type
    D->>B: アクセストークン・リフレッシュトークン等返却
    
    %% Discord ユーザー情報取得・保存
    B->>D: GET /users/@me
    D->>B: Discord ユーザー情報
    B->>B: discord_user テーブルに保存
    
    %% Bot によるサーバー追加
    B->>Bot: ユーザーをサーバーに追加
    Note right of Bot: DISCORD_BOT_TOKEN で認証
    Bot->>D: PUT /guilds/{guild.id}/members/{user.id}
    D->>Bot: 成功レスポンス
    Bot->>B: 完了通知
    B->>B: discord_guild_member テーブルに保存
    
    %% 完了通知
    B->>F: 成功レスポンス
    F->>U: 完了トースト表示
```

## アクセストークンに必要なスコープ
| スコープ名 | 説明 | 認可画面に表示される文言 |
|------------|------|------------------------|
| `identify` | allows /users/@me without email | ユーザー名、アバター画像、バナーにアクセス |
| `guilds.join` | allows /guilds/{guild.id}/members/{user.id} to be used for joining users to a guild | サーバーに参加します |

## 実際の流れ
|1. mond のメンバーシップを購読開始する|2. Discord アカウント情報へのアクセスを mond に許可する|3. Bot によってDiscord サーバーに追加される|
|---|---|---|
|![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/13a7c3c7-a7f0-414d-adcb-9be5fba5a41d.png)|![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/083a6113-8004-ad2b-465a-d8a388c4e0a4.png)|![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/b3ba190f-94fd-5461-f950-cc7c33792dbc.png)|

# おわりに
今回は mond における Discord 連携の設計に関する記事を書いてみました。
だいぶ端折っているので、年末年始に余裕があれば設計の続きと実装編も書こうと思います。

弊社のアドベントカレンダーで mond に関する記事が他にも公開されていますので、ご興味があれば覗いてみてください。

https://qiita.com/KotaroSasaki/items/021738713cc499ed75c9

https://blog.howtelevision.co.jp/entry/2024/12/06/140704