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作成では使用できません。
参考:
Activityとは
- コミュニケーションアプリDiscordにおいて、「ボイスチャンネル(音声通話機能)」を使用しているユーザーが、同じゲームやアプリを同時に実行したりするための仕組み
- 複数人でホワイトボードを共有できる
Whiteboard
や、同じYouTube動画を視聴できるWatch Together
などがある
例えばWindowsのDiscordアプリでは、任意のサーバーのボイスチャンネルに入り、左下にあるロケットのアイコンから様々なアクティビティを起動することができます。
開発者的な視点でいえば、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のページなんかは開けません)
注意:開発者プレビューによる制限
2024年4月現在、Discord Embedded App Sdkは「開発者プレビュー」状態であり、いくつかの制限があります。
(公式解説ページの各ページ頭にもその旨が書いてありますが、英語が苦手な私は無意識にスルーしていました。)
引用します。
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月)現在のものを書いておきます。
アプリケーションの登録
ページ右上New Application
からアプリケーションの登録を行います。
アプリの名前を適当に設定します。
ここまでは、ActivityでないDiscord Botと同じです。
左側メニューACTIVITIES
内のGetting Started
をクリックし、ページ下部のGet Started
を押します。
ACTIVITIES
内にURL Mappings
、Settings
、Art Assets
の3タブが出現していることを確認します。
IDとシークレットを取得
SETTINGS
のOAuth2
をクリックします。
すると、Client information
という領域の中に、CLIENT ID
とCLIENT SECRET
があります。
まずCLIENT IDをコピーし、保存しておきます。
次にこのCLIENT SECRET
のReset Secret
を押して、新たなシークレットを生成します。
シークレットを生成するとコピーできるようになりますので、そちらも保存しておきます。
さらに、Redirects
も設定しておきます。
OAuth2認証を行う際に、URIの検証を行うようです。これをスキップすると、Activityが正常に動作しなくなります。
Add Redirect
ボタンを押します。
公式チュートリアル同様に、プレースホルダとしてhttp://127.0.0.1
を設定してSave Changes
します。
ここの意味は正直分かっていませんので、情報をお待ちしています。
これで、Discord Activityを開発する準備ができました。
この開発者ポータルは、あとでまた開きます。
2. アプリの作成
ここからは、実際のアプリケーションのコードを作成していきます。
node
とyarn
は予め使用可能な状態にしておきます。
2.1. yarnで初期化
自分の都合の良い場所にフォルダを作成し、その中でyarn initを行います。
ここでは、-y
(yesフラグ)を使用して初期化時の質問を省略しました。
また、以降のファイルの編集はvscodeで行いました。
mkdir ActivityTest1
cd ActivityTest1/
yarn init -y
code .
こんな感じに、package.jsonが出来ている状態です。
ここにserver.js
を作成して、動作を確認してみます。
console.log('Hello, World!');
私は、VSCodeメニューのTerminal
→New Terminal
またはCtrl+Shift+@
でターミナルを開き、nodeでserver.js
ファイルを実行しました。
node server.js
2.2. ライブラリを追加
yarn add
コマンドを使い、フォルダにJavaScriptのライブラリを追加していきます。
今回は前述しましたように、サーバー作成用のexpress
、公開するJSファイルをまとめて生成してくれるビルドツールwebpack
、そしてEmbedded App Sdk
を使用していきます。
@discord/embedded-app-sdk
の@
は、パッケージを@ユーザー名/パッケージ名
と指定するためのものだそうです。
参考:https://blog.katio.net/page/onokatio-adc2019-day15-blog-react-1
yarn add express webpack @discord/embedded-app-sdk
2.3. ページを公開するコードを作成
ライブラリを追加しましたので、まずはサーバーのコードを作成していきます。
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
ファイルを作成します。
<!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のブラウザからページにアクセスできるようになります。
node server.js
http://localhost:3000 に移動し、ページを確認しましょう。
これで、サーバーをローカルで立てられることを確認できました。
2.4. 残りのコードを作成
サーバーをローカルで立てられることが確認できましたので、残りのコードをまとめて作成していきます。
まずは、server.js
のコードを編集し、/api/token
へのアクセス時には、DiscordのAPIからTokenを持ってくるようにします。
ActivityのIDとシークレットは、環境変数から取得する仕組みになっています。
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}番ポートで起動しました。`);
});
詳細は後述しますが、技術的には/api/token
を介さず、直接DiscordのAPIを叩いてtokenを取得することも可能だと思います。
しかし、その場合はクライアント側にDiscord Activityのシークレットを公開することになってしまいますので、セキュリティ上完全にアウトです。
ですので、公式チュートリアルでもtokenの取得はいったんサーバーを介すようになっていますし、ここでもそのようにしていきます。
続いて、src
フォルダを作成し、その中にindex.js
を追加します。これがDiscordアプリと公開したSPAの間のやりとりを担当してくれる、このアプリの本体ともいえるJavaScriptファイルです。
TODO: 上から4行目、client_id=
のところに、先ほど取得したIDを埋め込んでください。
ここにIDを書くとブラウザ側からIDが丸見えなので不安を感じる方もいるかもしれません。
しかし、後述するDiscord Canary
を使うと、例えばWatch Together
などの公式ActivityのIDも筒抜けになっていることがわかります。
そういうものと考えるのがいいと思います。
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ビルドを行います。
npx webpack
これにより、自分で作成したsrc/index.js
とダウンロードしてきた@discord/embedded-app-sdk
、そのほか諸々が結合されて、dist/main.js
が生成されます。
色々警告が表示されますが、生成自体は無事にできているので問題ありません。
テスト実行します。
node server.js
再び http://localhost:3000 を開いて、ページを確認します。
ここで、Edge
ならF12
キーを押すなどして、開発者ツールのコンソールを開きます。
以下のようなエラーが表示されていれば成功です。
このframe_id
は、DiscordからActivityとしてページにアクセスした場合にのみ定義されていますので、ブラウザからアクセスした際には、このようなエラーが出るのが正しい動きです。
3. デプロイ
ここからは、作ったアプリを実際に公開し、Discord上で動作を確認していきます。
公開といってもページがネット上に公開されるだけで、実際にだれでもDiscord上で起動できるようになるわけではありませんので注意してください。
また、前述の開発者プレビュー制限により、メンバー25名以上のサーバーではActivityのテストを行うことができませんので、その場合は新しいDiscordサーバーを作成しましょう。
cloudflaredを使用できる場合は3-a、できない場合は3-bをお勧めします。
3-a. cloudflaredを使って公開
cloudflaredは、アメリカのネットワーク企業cloudflareが提供している、トンネルを作成することでローカルのサーバーを簡易的に外部に公開できるツールです。
(正直自分がどのようにcloudflaredを導入したのか覚えていないので)参考リンクを貼っておきます。
cloudflareのアカウントを持っていない方は作成する必要があるはずです。
3-a.1. cloudflaredでサーバー公開
まずは2節同様、server.js
を動かしていくのですが、IDをシークレットを環境変数で指定してあげる必要があります。
以下のコマンドのXXXを先ほど保存したID、YYYを先ほど保存したシークレットで置き換えてください。
DISCORD_CLIENT_ID="XXX" DISCORD_CLIENT_SECRET="YYY" node server.js
次に、別のターミナルから、localhost:3000を世界に公開するためのコマンドを打ちます。
cloudflared tunnel --url http://localhost:3000
それなりの量のログが出ますが、注目は図のように四角で囲まれているURLです。
このURLが今localhostと繋がっているURLであり、試しにブラウザからアクセスしてみると、先ほど同様にHello Worldページが表示されるはずです。
このURLは、cloudflaredコマンドを使用するたびに変わります。
3-a.2. Discord Activityの設定
Discord Activityに、用意したURLを設定していきます。
Discordの開発者ポータルから先ほど作成したアプリのページを開きます。
そして、左側メニューACTIVITIES
のURL Mappings
を開き、URL Mappings
のTARGET
に先ほどのURLをコピペします。
そしてSave Changes
を押します。
これで、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の管理から除外します。
/node_modules/
/dist/main.js
そして、フォルダをgit管理します。適当なコメントを付けてコミットを行います。
git init
git add .
git commit -m "initial commit"
3-b.2. GitHubにPush
GitHubにログインし、新しいリポジトリを作成します。
私は、ActivityTest1
というプライベートリポジトリを作成しました。
ページ下部、...or push an existing repository from the command line
をコピーして、先ほどgit管理したフォルダで実行します。
リポジトリが更新されていることを確認したら、pushは成功です。
3-b.3. Renderを設定
次に、Render.comの設定を進めていきます。
記事などを参考にしながら、アカウントの新規登録を行います。
GitHubアカウントを持っていれば簡単に作ることができます。
参考記事:
ここから、実際にアプリのデプロイ設定をしていきます。
ダッシュボードを開きます。
まだ開始しているサービスが1つもない場合は下のように表示されますので、赤丸のNew Web Service
を押して設定を開始します。すでにサービスを作成したことがある場合は右上のNew+
からWeb Service
を選ぶことができます。
Build and Deploy from a Git Repository
とありますので、このままNext
します。
まだRenderとGitHubの連携を行っていない場合、Connect a Repository
内にConnect Repository
ボタンがあります。ここをクリックして、適宜RenderとGitHubの連携を設定(インストール)します。
All repositories(すべてのリポジトリを連携)
かOnly select repositories(特定のリポジトリのみを連携)
かは好みにお任せします。Only select repositories
の設定にする場合は、先ほどpushしたリポジトリ(ここではActivityTest1
)を選択していることを確認してください。
再びConnect a Repository
に戻りますので、先ほどpushしたリポジトリを選択します。
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
を押してサービスを開始します。
サービスの管理画面に開きます。ログを眺めながらデプロイが完了するのを待ちましょう。
Your service is live
と表示されたらデプロイ完了です。
デプロイが完了したら、ページの上方にあるURLをクリックして、相変わらず真っ白なHello World
ページが表示されることを確認しましょう。
あとで使用するので、この時のURLを保存しておいてください。
3-b.3. Discord Activityの設定
Discord Activityに、保存したURLを設定していきます。
Discordの開発者ポータルから先ほど作成したアプリのページを開きます。
そして、左側メニューACTIVITIES
のURL Mappings
を開き、URL Mappings
のTARGET
に先ほどのURLをコピペします。
そしてSave Changes
を押します。
これで、DiscordでActivityを使用する設定ができました。
4. Discord/Discord Canaryを使用した確認とデバッグ
ここから実際に、Discordを使用した動作確認を行っていきます。
開発者モードの有効化
私が使用している、Windowsのデスクトップアプリ版Discordを使用して解説します。
また、Discordのインストールやアカウントの作成に関しては省略します。
画面左下のメニューの歯車ボタンを押して、設定画面を開きます。
左側のメニューのアプリの設定
カテゴリにある詳細設定
を押します。
これで、開発中のActivityを起動する準備ができました。
Activityの起動
いよいよ実際にActivityを起動してみます。
まず、Activityを起動するための、メンバー25人未満のサーバーを準備します。持っていない場合は自分でサーバーを作成するとよいでしょう。簡単にできます。
用意したサーバーのボイスチャンネルに入ります。
左下のメニューからロケットのマークを選択し、Activity起動画面を開きます。
すると、先ほど作ったアクティビティの名前が表示されているはずですので、そのアクティビティを起動します。
Hello Worldが表示され、少し経ってサーバーのアイコンと入っているボイスチャンネルの名前が表示されたら成功です!
(失敗した場合、次に紹介するDiscord Canaryを入れておくと、デバッグの役に立つかもしれません。)
Discord Canaryの使用
ここまででActivityが動作することを確認できましたが、ログなどの確認ができないと不便です。
そこで、Discord Canaryを使います。
Discord CanaryはDiscordのアルファ版であり、バグの可能性のある最新機能が反映されるバージョンです。
このDiscord Canaryには、Discord内で発生したログを見ることができるという機能がありますので、これを濫用する訳です。
アルファ版故に入手場所が変わる可能性もありますので、都度検索してDiscord Canaryをダウンロードしてください。
Discord Canaryをインストールしたら、通常のDiscord同様にログインして、Activityを起動します。
ここで、Ctrl+Shift+I
キーを押すことで、ブラウザの開発者ツールに似た画面が右側に開きます。
Activity以外にも様々なログが出ます。
上部のFilterにRpcApplicationLogger
と入力すると、起動しているActivityのログを見ることができるようです。
また前述しましたように、この機能を使えばあらゆるActivityのIDを見ることができます。
動作チェック
公式ページにある図を見ながら、今回のアプリの動作について確認していきます。
(間違えている可能性があります。)
図のDiscord-Client
がDiscordのアプリ、Application-Iframe
がHTMLファイル、Discord API
がDiscord公式の提供しているWebAPI、Application-Server
が自分で用意したAPIサーバーです。
まず、Discordが設定されたURLをもとにページを取得、Discordにマウントします。
その後、アプリの情報をDiscord APIから取得し、ページはDiscordから準備が完了したことを受け取ります。
// DiscordSDKのインスタンスを生成
const discordSdk = new DiscordSDK(client_id);
// DiscordSDKの初期化
await discordSdk.ready();
console.log("DiscordSDK is ready");
その後、欲しいスコープを渡してauthorize code
を取得。
一度サーバーを介して(シークレットがサーバー内に保存されているため)DiscordのAPIにアクセスし、tokenを受け取る。
そして、Discordのアプリにトークンを渡して、Discordからデータを受け取れるようにする。
ここまでが、Discord Embedded App Sdkの開始処理といえそうです。
// 認証処理
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月現在)もう少し時間がかかりそうです。