できる限り簡単にDiscord Embedded App Sdkを動作させる

# Discord Embedded App Sdk
公式のチュートリアルがやや必要なファイルや予備知識が多いと感じたので、自分用にシンプルなチュートリアルを作成しました。
筆者はWeb開発およびJavaScriptに関しては初心者です。
かなり雑な設計になっておりますので、動作の仕組みがわかった後、本番の開発はもっと作りやすいアーキテクチャを使用するのがおすすめです。

## 前提知識
- Discord Embedded App Sdkは、Discord公式によって最近（2024年3月）提供された、Discord Activityを開発するためのライブラリ
- 現在（2024年4月）SDKは**開発者プレビュー状態**のため、今後仕様などが大きく変わる可能性がある
- また、関連ライブラリなどの周辺状況も変わっていく可能性がある

### 一般的なDiscord BotとDiscord Activityの違い

知っている方も多いでしょうが、Discordでは以前からBotを作成することができました。
そのBotでは、テキストメッセージの受信や送信、音声の受信や送信などの機能を使うことができました。
しかし、DiscordのActivityという機能を、一般の開発者が自由に作ることはできませんでした。
そのために作られたライブラリがDiscord Embedded App Sdkなのです。
ですので、通常のDiscord BotとDiscord Embedded App Sdkによって作れるActivityとでは、できることが全く違うことがわかります。
また、当然Discord GoのようなDiscord Botを簡単に作成できるライブラリは、Activity作成では使用できません。
参考：

https://qiita.com/sugar_king777/items/8fe65c5abecce0376f56

### Activityとは
- コミュニケーションアプリDiscordにおいて、「ボイスチャンネル（音声通話機能）」を使用しているユーザーが、同じゲームやアプリを同時に実行したりするための仕組み
- 複数人でホワイトボードを共有できる`Whiteboard`や、同じYouTube動画を視聴できる`Watch Together`などがある

例えばWindowsのDiscordアプリでは、任意のサーバーのボイスチャンネルに入り、左下にあるロケットのアイコンから様々なアクティビティを起動することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/52ac96d0-a3b9-43ae-66b2-c96a620e2b8a.png)

開発者的な視点でいえば、Activityは、Discordというアプリに他のアプリを組み込んで動作させる仕組みといえるかもしれません。

### Activityの仕組み

これは実際に体験してみる方が早く理解できると思うのですが、一応言葉での説明をつらつらと並べておきます。
まずはじめに、Acitivityは **HTMLページ, SPA=Single Page Application** として開発します。
つまり、通常のWebページと同様にActivity用のページを公開し、そのURLをDiscordの開発者ページから設定することで、Activityを使用可能になるという仕組みです。
別の視点から見るならば、Discord上でActivityを起動すると、Discordは予め設定されたURLにページを取得しにいき、そのページの内容をDiscord上に組み込んで表示するということです。
こういう仕組みなので、おそらく、通常のHTMLページ+JavaScriptでできるようなことは大体できると思います。

### Discord Embedded App Sdk

Embedded App Sdkは、前述のようにActivityを開発するためのJavaScriptライブラリです。
Embedded App Sdkを使って作成したSPAを公開し、Discordの開発者ポータルからその公開URLを指定することで、Activityを動かすことができます。

こういった仕組みですので、例えば自作Activityの宛先URLに、`GitHub Pages`なんかで作られたシンプルな公開ページのURLを指定すると、Discord上でそのページを開けたりします。（Qiitaのページなんかは開けません）

### :exclamation::exclamation: 注意：開発者プレビューによる制限:exclamation::exclamation: 

2024年4月現在、Discord Embedded App Sdkは「開発者プレビュー」状態であり、いくつかの制限があります。
（公式解説ページの各ページ頭にもその旨が書いてありますが、英語が苦手な私は無意識にスルーしていました。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e35eae4d-f975-74c6-2dee-ea2492bdbf1d.png)

https://discord.com/developers/docs/activities/overview#public-developer-preview

引用します。

>Public Developer Preview
>Building Activities with the Embedded App SDK is currently in developer preview. During this initial preview, developers will find some limitations that will be removed as we continue to release developer features and functionality for Activities.
>
>Current limitations include:
>
> Activities will only be playable in servers with 25 users or less
> In-App Purchases (IAP) are possible within Activities but not yet available to developers in this Developer Preview
> Activities cannot become Verified in this Developer Preview
> Existing Verified Apps cannot add Activities during this Developer Preview

要は、開発者プレビューでは、

- メンバー数が25人以下のサーバーでしか利用できない
- アプリ内購入が利用できない
- Verified（認証済み）にすることができない

といった制限が現状あり、今後段階的に緩和していくということのようです。

特に「**メンバー数が25人以下のサーバーでしか利用できない**」の制限が大きいですね。

## 実際に開発してみる
何はともあれ、実際に開発していきます。

### 0. 環境
#### 開発環境
- Windows 11
- WSL Ubuntu 22.04.3 LTS
- VSCode v1.86.2（WSL拡張を使用）

#### テスト・確認アプリ
- Discord（アプリ版）
- Discord Canary

#### 使用言語・フレームワークなど
- JavaScript
- node.js v18.19.1
- yarn

#### 使用ライブラリ
- express
- discord/embedded-app-sdk
- Webpack

### 1. Discord開発者ポータルを使って設定

まず一番初めにすることは、Discordの開発者ポータルから作成するアプリを登録することです。
この辺りは更新によって今後変わっていく可能性がありますが、記事執筆時（2024年4月）現在のものを書いておきます。

#### アプリケーションの登録

https://discord.com/developers/applications

ページ右上`New Application`からアプリケーションの登録を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/4ffa3568-3830-3aaa-0da5-dc932ad4d7cc.png)

アプリの名前を適当に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/95e78267-b4d9-6947-7dd9-25d37f4cd647.png)

::: note info
ここまでは、ActivityでないDiscord Botと同じです。
:::

左側メニュー`ACTIVITIES`内の`Getting Started`をクリックし、ページ下部の`Get Started`を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/1603b87f-19d9-c715-d20b-d92297d91fc3.png)

`ACTIVITIES`内に`URL Mappings`、`Settings`、`Art Assets`の3タブが出現していることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/f74ddd8f-30c7-6534-4dd7-0839f99482d5.png)

#### IDとシークレットを取得

`SETTINGS`の`OAuth2`をクリックします。

すると、`Client information`という領域の中に、`CLIENT ID`と`CLIENT SECRET`があります。
まずCLIENT IDをコピーし、保存しておきます。
次にこの`CLIENT SECRET`の`Reset Secret`を押して、新たなシークレットを生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/569c00e9-99eb-aa59-a4e3-0817e5285b34.png)
シークレットを生成するとコピーできるようになりますので、そちらも保存しておきます。

さらに、`Redirects`も設定しておきます。

OAuth2認証を行う際に、URIの検証を行うようです。これをスキップすると、Activityが正常に動作しなくなります。

`Add Redirect`ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e45c2302-f9c2-a962-5b71-9aa59141d452.png)

公式チュートリアル同様に、プレースホルダとして`http://127.0.0.1`を設定して`Save Changes`します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/f4a34cf4-8f0b-0f88-3803-265d302da21f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/917377cd-14d3-4d98-076f-ca68c056ee4c.png)

::: note warn
ここの意味は正直分かっていませんので、情報をお待ちしています。
:::

これで、Discord Activityを開発する準備ができました。

::: note info
この開発者ポータルは、あとでまた開きます。
:::

### 2. アプリの作成

ここからは、実際のアプリケーションのコードを作成していきます。
`node`と`yarn`は予め使用可能な状態にしておきます。

#### 2.1. yarnで初期化
自分の都合の良い場所にフォルダを作成し、その中でyarn initを行います。
ここでは、`-y`（yesフラグ）を使用して初期化時の質問を省略しました。
また、以降のファイルの編集はvscodeで行いました。

```shell:WSL
mkdir ActivityTest1
cd ActivityTest1/
yarn init -y
code .
```

こんな感じに、package.jsonが出来ている状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/97c00070-826f-63f4-eb5f-2ffdfa89b432.png)

ここに`server.js`を作成して、動作を確認してみます。

```server.js
console.log('Hello, World!');
```

私は、VSCodeメニューの`Terminal`→`New Terminal`または`Ctrl+Shift+@`でターミナルを開き、nodeで`server.js`ファイルを実行しました。

```shell:WSL
node server.js
```

無事にHello Worldが表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/b576a5d5-93a1-7e1b-2791-d4535c97728f.png)


#### 2.2. ライブラリを追加

`yarn add`コマンドを使い、フォルダにJavaScriptのライブラリを追加していきます。
今回は前述しましたように、サーバー作成用の`express`、公開するJSファイルをまとめて生成してくれるビルドツール`webpack`、そして`Embedded App Sdk`を使用していきます。

::: note info
`@discord/embedded-app-sdk`の`@`は、パッケージを`@ユーザー名/パッケージ名`と指定するためのものだそうです。
参考：https://blog.katio.net/page/onokatio-adc2019-day15-blog-react-1
:::

```shell:WSL
yarn add express webpack @discord/embedded-app-sdk
```

#### 2.3. ページを公開するコードを作成

ライブラリを追加しましたので、まずはサーバーのコードを作成していきます。

```server.js
const express = require('express');
const app = express();
const PORT = 3000; // ポート番号は3000

// distディレクトリ内のファイルを返す
app.use(express.static('dist'));

// サーバーを起動
app.listen(PORT, () => {
    console.log(`サーバーが${PORT}番ポートで起動しました。`);
});

```

次に、公開用の仮のHTMLファイルを作成します。
`dist/`フォルダを作成し、その中に`index.html`ファイルを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/54244479-0c77-34d9-47d2-9050dd1b2694.png)

```index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>Sample Page</title>
</head>

<body>
    <h1>Hello, World!</h1>
</body>

</html>

```

これでserver.jsを起動すれば、自分のPCのブラウザからページにアクセスできるようになります。

```shell:WSL
node server.js
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/a6323f55-8387-8f23-9e50-891110c252a1.png)

http://localhost:3000 に移動し、ページを確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/388b84b5-ff3e-3e97-d8e4-add41ae46d2f.png)

これで、サーバーをローカルで立てられることを確認できました。

#### 2.4. 残りのコードを作成

サーバーをローカルで立てられることが確認できましたので、残りのコードをまとめて作成していきます。

まずは、`server.js`のコードを編集し、`/api/token`へのアクセス時には、DiscordのAPIからTokenを持ってくるようにします。

ActivityのIDとシークレットは、環境変数から取得する仕組みになっています。

```server.js
const express = require('express');
const app = express();
const PORT = 3000; // ポート番号は3000

// JSONを受け取るための設定
app.use(express.json());

// /api/tokenへのPOSTリクエストを処理を登録
app.post('/api/token', async (req, res) => {
    // token取得用のcodeを受け取る
    code = req.body.code;
    console.log("code:", code);

    // DiscordのAPIを叩いてtokenを取得
    params = new URLSearchParams({
        client_id: process.env.DISCORD_CLIENT_ID, // 環境変数から取得
        client_secret: process.env.DISCORD_CLIENT_SECRET, // 環境変数から取得
        grant_type: "authorization_code",
        code: code,
    });
    console.log("params:", params);

    const response = await fetch(`https://discord.com/api/oauth2/token`, {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
        },
        body: params,
    });
    const { access_token } = await response.json();
    console.log("access token:", access_token);

    // tokenを返す
    res.json({ access_token });
});

// /app/token以外のアクセスには、distディレクトリ内のファイルを返す
app.use(express.static('dist'));

// サーバーを起動
app.listen(PORT, () => {
    console.log(`サーバーが${PORT}番ポートで起動しました。`);
});
```

::: note info
詳細は後述しますが、技術的には`/api/token`を介さず、直接DiscordのAPIを叩いてtokenを取得することも可能だと思います。
しかし、その場合はクライアント側にDiscord Activityのシークレットを公開することになってしまいますので、セキュリティ上完全にアウトです。
ですので、公式チュートリアルでもtokenの取得はいったんサーバーを介すようになっていますし、ここでもそのようにしていきます。
:::

続いて、`src`フォルダを作成し、その中に`index.js`を追加します。これがDiscordアプリと公開したSPAの間のやりとりを担当してくれる、このアプリの本体ともいえるJavaScriptファイルです。

**TODO:** 上から4行目、`client_id=`のところに、先ほど取得したIDを埋め込んでください。

::: note info
ここにIDを書くとブラウザ側からIDが丸見えなので不安を感じる方もいるかもしれません。
しかし、後述する`Discord Canary`を使うと、例えば`Watch Together`などの公式ActivityのIDも筒抜けになっていることがわかります。
そういうものと考えるのがいいと思います。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/419a48a9-83b1-51ce-7abd-70a4c46b5742.png)

```index.js
import { DiscordSDK } from "@discord/embedded-app-sdk";

// TODO: ここにDiscordアプリケーションのクライアントIDを入力
const client_id = "XXX"

// DiscordSDKのインスタンスを生成
const discordSdk = new DiscordSDK(client_id);

// DiscordSDKの初期設定
async function setupDiscordSdk() {
    let auth = null;
    console.log("DiscordSDK is setting up");

    // DiscordSDKの初期化
    await discordSdk.ready();
    console.log("DiscordSDK is ready");

    // 認証処理
    const { code } = await discordSdk.commands.authorize({
        client_id: client_id, // クライアントIDを取得
        response_type: "code",
        state: "",
        prompt: "none",
        scope: [
            "identify",
            "guilds",
        ],
    });
    console.log("Authorization is successful");

    // トークンの取得
    const response = await fetch("/api/token", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            code,
        }),
    });
    console.log("Token is fetched");

    const { access_token } = await response.json();
    // 認可処理
    auth = await discordSdk.commands.authenticate({
        access_token,
    });
    console.log("Authentication is successful");

    if (auth == null) {
        throw new Error("Authenticate command failed");
    }

    return auth;
}

(async () => {
    try{
        let auth = await setupDiscordSdk();
        console.log("DiscordSDK is set up");

        // channel nameを表示
        const channel = await discordSdk.commands.getChannel({ channel_id: discordSdk.channelId });
        document.getElementById("channel-name").innerText = channel.name;

        // guild iconを表示
        const guilds = await fetch(`https://discord.com/api/v10/users/@me/guilds`, {
            headers: {
                Authorization: `Bearer ${auth.access_token}`,
                'Content-Type': 'application/json',
            },
        });
        const guildsJson = await guilds.json(); // サーバー情報を取得
        const currentGuild = guildsJson.find((g) => g.id === discordSdk.guildId);
        const guildImg = document.createElement('img');
        guildImg.setAttribute(
            'src',
            `https://cdn.discordapp.com/icons/${currentGuild.id}/${currentGuild.icon}.webp?size=128`
        );
        document.getElementById("guild-icon").appendChild(guildImg);
    }catch(e){
        console.error(e);
    }
})();

```

#### 2.5. Webpackビルドしてテスト

Webpackビルドを行います。

```shell:WSL
npx webpack
```

これにより、自分で作成した`src/index.js`とダウンロードしてきた`@discord/embedded-app-sdk`、そのほか諸々が結合されて、`dist/main.js`が生成されます。
色々警告が表示されますが、生成自体は無事にできているので問題ありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/9e467bfe-3d8b-b805-9d35-aaa3716f0708.png)

テスト実行します。

```shell:WSL
node server.js
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/18c7f18f-f2d0-5ed9-2afc-86b40ad238c1.png)

再び http://localhost:3000 を開いて、ページを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e49efba4-661a-fc89-7978-a2f4d35b5361.png)

ここで、`Edge`なら`F12`キーを押すなどして、開発者ツールのコンソールを開きます。
以下のようなエラーが表示されていれば**成功**です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/25002466-91c9-abd1-396a-b5c197a2b148.png)

この`frame_id`は、DiscordからActivityとしてページにアクセスした場合にのみ定義されていますので、ブラウザからアクセスした際には、このようなエラーが出るのが正しい動きです。

### 3. デプロイ

ここからは、作ったアプリを実際に公開し、Discord上で動作を確認していきます。
公開といってもページがネット上に公開されるだけで、実際にだれでもDiscord上で起動できるようになるわけではありませんので注意してください。
また、前述の開発者プレビュー制限により、メンバー25名以上のサーバーではActivityのテストを行うことができませんので、その場合は新しいDiscordサーバーを作成しましょう。

cloudflaredを使用できる場合は3-a、できない場合は3-bをお勧めします。

### 3-a. cloudflaredを使って公開
cloudflaredは、アメリカのネットワーク企業cloudflareが提供している、トンネルを作成することでローカルのサーバーを簡易的に外部に公開できるツールです。

（正直自分がどのようにcloudflaredを導入したのか覚えていないので）参考リンクを貼っておきます。
cloudflareのアカウントを持っていない方は作成する必要があるはずです。

https://qiita.com/harutiro/items/885c0df9662a0c6e6355

#### 3-a.1. cloudflaredでサーバー公開

まずは2節同様、`server.js`を動かしていくのですが、IDをシークレットを環境変数で指定してあげる必要があります。
以下のコマンドのXXXを先ほど保存したID、YYYを先ほど保存したシークレットで置き換えてください。
```shell:WSL
DISCORD_CLIENT_ID="XXX" DISCORD_CLIENT_SECRET="YYY" node server.js
```

次に、別のターミナルから、localhost:3000を世界に公開するためのコマンドを打ちます。

```shell:WSL
cloudflared tunnel --url http://localhost:3000
```
それなりの量のログが出ますが、注目は図のように四角で囲まれているURLです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/888700bd-224b-e7f7-8d0a-88f9ef2f3145.png)

このURLが今localhostと繋がっているURLであり、試しにブラウザからアクセスしてみると、先ほど同様にHello Worldページが表示されるはずです。
**このURLは、cloudflaredコマンドを使用するたびに変わります。**

#### 3-a.2. Discord Activityの設定

Discord Activityに、用意したURLを設定していきます。
Discordの開発者ポータルから先ほど作成したアプリのページを開きます。

https://discord.com/developers/applications

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/b4f52742-d07d-a85f-fc4e-9e1a44926de5.png)

そして、左側メニュー`ACTIVITIES`の`URL Mappings`を開き、`URL Mappings`の`TARGET`に先ほどのURLをコピペします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/16e75f63-4a21-c3c8-9fb2-26cc28368bbe.png)

そして`Save Changes`を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/5c3d73ef-4e94-451e-08c9-cc05872590d1.png)

これで、DiscordでActivityを使用する設定ができました。
**サーバー・cloudflaredともに起動したまま**、次節に移っていきましょう。

### 3-b. Renderを使ってデプロイ

Renderは、簡単に作成したWebアプリをデプロイすることができるWebサービスです。
cloudflaredを使えない・ローカルからトンネルするだけでなく、実際にデプロイしてみたい、という方はこちらの方法を使うのが良いでしょう。

#### 3-b.1. Git管理の開始

Renderを使ってデプロイする場合には、GitHubのリポジトリを使用するのが簡単です。

まず、`.gitignore`ファイルを作成していきます。
yarnが作成した`node_modules`と、webpackが作成した`dist/main.json`をgitの管理から除外します。
```:.gitignore
/node_modules/
/dist/main.js
```

そして、フォルダをgit管理します。適当なコメントを付けてコミットを行います。

```shell:WSL
git init
git add .
git commit -m "initial commit"
```

#### 3-b.2. GitHubにPush

GitHubにログインし、新しいリポジトリを作成します。

https://github.com/

私は、`ActivityTest1`というプライベートリポジトリを作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/c233720c-d75a-81d1-0114-9621fa5e6c83.png)

ページ下部、`...or push an existing repository from the command line`をコピーして、先ほどgit管理したフォルダで実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/4b55da21-6983-f6a5-aecf-0617e571fe95.png)

リポジトリが更新されていることを確認したら、pushは成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/eedd9adf-1ad6-9084-c8bd-f12f11f8529b.png)


#### 3-b.3. Renderを設定

次に、Render.comの設定を進めていきます。

https://render.com/

記事などを参考にしながら、アカウントの新規登録を行います。
GitHubアカウントを持っていれば簡単に作ることができます。
参考記事：

https://zenn.dev/protoout/articles/54-howto-render-setup

ここから、実際にアプリのデプロイ設定をしていきます。
ダッシュボードを開きます。

https://dashboard.render.com/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/d9022e42-34ba-0d9d-55f0-702311303305.png)

まだ開始しているサービスが1つもない場合は下のように表示されますので、赤丸の`New Web Service`を押して設定を開始します。すでにサービスを作成したことがある場合は右上の`New+`から`Web Service`を選ぶことができます。

`Build and Deploy from a Git Repository`とありますので、このまま`Next`します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/8c9118cd-f388-f146-9628-1884a915fcb1.png)

まだRenderとGitHubの連携を行っていない場合、`Connect a Repository`内に`Connect Repository`ボタンがあります。ここをクリックして、適宜RenderとGitHubの連携を設定（インストール）します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e125dc47-99ff-fd7b-7885-3c2194dc9815.png)

`All repositories(すべてのリポジトリを連携)`か`Only select repositories(特定のリポジトリのみを連携)`かは好みにお任せします。`Only select repositories`の設定にする場合は、先ほどpushしたリポジトリ（ここでは`ActivityTest1`）を選択していることを確認してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/41e14c9e-a47c-84a0-90ae-d3ef034afa7e.png)

再び`Connect a Repository`に戻りますので、先ほどpushしたリポジトリを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e9620665-6ce2-821b-7ed3-06b29c2ec991.png)

`You are deploying a web service for ~~`のように表示され、デプロイ設定画面になります。
各設定項目は以下のようにしていきましょう。
- Name: サービス名。そのままでよいです。私は`ActivityTest1`となりました
- Region: おそらくサーバー位置。そのままでよいです。地理的に`Singapore`にしても良いかもしれません
- Branch: GitHubのブランチ。先ほどの手順を進めていれば`main`になっていると思いますので、そのままで良いです
- Root Directory: ルートフォルダ。今回の構成ではそのままでよいです
- RunTime: 今回はnodeで動かせるようにしてあるので`Node`のままでよいです
- **Build Command**: `yarn && npx webpack`とします。これで`node_module`と`main.js`を生成します
- **Start Command**: 今回は`server.js`をエントリーポイントとしたので、`node server.js`とします
- **Instance Type**: `Free`を選択します。`Free`の場合、**一定時間アクセスがないとサービスが停止され、次のアクセス時に数分ほどの再起動が発生する**ことに注意してください
- **Environment Variables**: 環境変数を設定します
    - `DISCORD_CLIENT_ID`: 先ほど保存したDiscord ActivityのIDを入力
    - `DISCORD_CLIENT_SECRET`: 先ほど保存したDiscord Activityのシークレットを入力

設定ができたら、`Create Web Service`を押してサービスを開始します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/11442da7-2632-9c55-8096-f5661003137e.png)

サービスの管理画面に開きます。ログを眺めながらデプロイが完了するのを待ちましょう。
`Your service is live`と表示されたらデプロイ完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e8d8867f-93fa-560e-1173-56c2bdf6f448.png)

デプロイが完了したら、ページの上方にあるURLをクリックして、相変わらず真っ白な`Hello World`ページが表示されることを確認しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e06357e0-c8ed-b747-e089-249f9a5699bc.png)

あとで使用するので、この時のURLを保存しておいてください。

#### 3-b.3. Discord Activityの設定

Discord Activityに、保存したURLを設定していきます。
Discordの開発者ポータルから先ほど作成したアプリのページを開きます。

https://discord.com/developers/applications

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/b4f52742-d07d-a85f-fc4e-9e1a44926de5.png)

そして、左側メニュー`ACTIVITIES`の`URL Mappings`を開き、`URL Mappings`の`TARGET`に先ほどのURLをコピペします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/0aaa5866-577d-8bac-9470-a53f3ecb3a3c.png)

そして`Save Changes`を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/5c3d73ef-4e94-451e-08c9-cc05872590d1.png)

これで、DiscordでActivityを使用する設定ができました。

### 4. Discord/Discord Canaryを使用した確認とデバッグ

ここから実際に、Discordを使用した動作確認を行っていきます。

#### 開発者モードの有効化

私が使用している、Windowsのデスクトップアプリ版Discordを使用して解説します。
また、Discordのインストールやアカウントの作成に関しては省略します。

画面左下のメニューの歯車ボタンを押して、設定画面を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/68ccf496-f821-ee22-8403-6b59d17469ba.png)

左側のメニューの`アプリの設定`カテゴリにある`詳細設定`を押します。

そうしたら、`開発者モード`のトグルスイッチをオンにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/a1ede43c-a614-8432-afc0-96c2993daefc.png)

これで、開発中のActivityを起動する準備ができました。

#### Activityの起動

いよいよ実際にActivityを起動してみます。

まず、Activityを起動するための、メンバー25人未満のサーバーを準備します。持っていない場合は自分でサーバーを作成するとよいでしょう。簡単にできます。

用意したサーバーのボイスチャンネルに入ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/a1b7b444-87b7-649e-99d0-96367ca879c5.png)

左下のメニューからロケットのマークを選択し、Activity起動画面を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/0331f07f-4d93-aa7b-016b-ac579145ef85.png)

すると、先ほど作ったアクティビティの名前が表示されているはずですので、そのアクティビティを起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/e4866c0b-e047-45d4-6282-19fdd1bd8717.png)

Hello Worldが表示され、少し経ってサーバーのアイコンと入っているボイスチャンネルの名前が表示されたら成功です！
（失敗した場合、次に紹介するDiscord Canaryを入れておくと、デバッグの役に立つかもしれません。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/21f138a4-6e14-1d2d-d903-58744fbb1ede.png)

#### Discord Canaryの使用

ここまででActivityが動作することを確認できましたが、ログなどの確認ができないと不便です。

そこで、Discord Canaryを使います。

Discord CanaryはDiscordのアルファ版であり、バグの可能性のある最新機能が反映されるバージョンです。
このDiscord Canaryには、Discord内で発生したログを見ることができるという機能がありますので、これを濫用する訳です。

アルファ版故に入手場所が変わる可能性もありますので、都度検索してDiscord Canaryをダウンロードしてください。
Discord Canaryをインストールしたら、通常のDiscord同様にログインして、Activityを起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/36792c16-dbdd-d32f-ceb7-cc3072e81aa3.png)

ここで、`Ctrl+Shift+I`キーを押すことで、ブラウザの開発者ツールに似た画面が右側に開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/bb259d5a-40c6-191c-173a-7d7a18ba8dce.png)

Activity以外にも様々なログが出ます。
上部のFilterに`RpcApplicationLogger`と入力すると、起動しているActivityのログを見ることができるようです。
また前述しましたように、この機能を使えばあらゆるActivityのIDを見ることができます。

### 動作チェック

公式ページにある図を見ながら、今回のアプリの動作について確認していきます。
（間違えている可能性があります。）

https://discord.com/developers/docs/activities/how-activities-work


図の`Discord-Client`がDiscordのアプリ、`Application-Iframe`がHTMLファイル、`Discord API`がDiscord公式の提供しているWebAPI、`Application-Server`が自分で用意したAPIサーバーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/8d78a218-9263-fb23-7f6c-f90c43fceeb7.png)

まず、Discordが設定されたURLをもとにページを取得、Discordにマウントします。
その後、アプリの情報をDiscord APIから取得し、ページはDiscordから準備が完了したことを受け取ります。
```javascript:index.js
// DiscordSDKのインスタンスを生成
const discordSdk = new DiscordSDK(client_id);
```
```javascript:index.js
    // DiscordSDKの初期化
    await discordSdk.ready();
    console.log("DiscordSDK is ready");
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3743934/cae7dab6-9f8e-bd35-028e-427448517ce5.png)

その後、欲しいスコープを渡して`authorize code`を取得。
一度サーバーを介して（シークレットがサーバー内に保存されているため）DiscordのAPIにアクセスし、tokenを受け取る。
そして、Discordのアプリにトークンを渡して、Discordからデータを受け取れるようにする。
ここまでが、Discord Embedded App Sdkの開始処理といえそうです。

```javascript:index.js

    // 認証処理
    const { code } = await discordSdk.commands.authorize({
        client_id: client_id, // クライアントIDを取得
        response_type: "code",
        state: "",
        prompt: "none",
        scope: [
            "identify",
            "guilds",
        ],
    });
    console.log("Authorization is successful");

    // トークンの取得
    const response = await fetch("/api/token", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            code,
        }),
    });
    console.log("Token is fetched");

    const { access_token } = await response.json();
    // 認可処理
    auth = await discordSdk.commands.authenticate({
        access_token,
    });
    console.log("Authentication is successful");

    if (auth == null) {
        throw new Error("Authenticate command failed");
    }

    return auth;
```

### まとめ、注意、展望

ここまでで、DiscordのActivityがなんとなく動作している状態にすることができたと思います。
この状態で`server.js`や`index.js`のコードをいじりながらどんな動作になるか確認してみると、Activityの動作に関する知見が深まっていくのではないかと思います。
（index.jsを変更した際は`npx webpack`を行わないと変更が反映されないことは注意）

繰り返しますが、今回のコードの構成はかなり単純化しています。
実際の開発時には、もっと複雑で（煩雑ではない）管理しやすいプロジェクト構成にする必要があるでしょう。

セキュリティ上致命的な弱点がある場合など、追記編集していきますので、コメントよろしくお願いします。

このチュートリアルを終えた後は、`TypeScript`や`React`などへの対応、ワークスペースとプロキシ機能を使って2サーバー構成にすることによって見通しをよくするなど、プロジェクト自体のより良い作りを模索してから、実際の開発に進んでいければと思います。
といっても、SDK周りのドキュメントや機能が充実するまでには、（2024年4月現在）もう少し時間がかかりそうです。