オンライン会議中のストレス駆動開発でボイスチャンネルにいるユーザーをランダム抽出する Discord bot を作った
こんにちは、やぐちはるおです。Hono と Cloudflare 使って Discord bot 作った話をします。と思ったんですが、書き終わってみたら Hono と Cloudflare の話はほとんどしてませんでした。
はじめに
会社で Discord 使ってるんですが、打ち合わせ中に誰でも良いから意見を欲しくなった時、誰を選ぶのかいつも困っています。ランダムに選ぼうと頑張ると、前回選んだ人とか直近よく喋っていた人とかを避けたりした方がいいかなとか、結局何かしら恣意的な選び方になってしまいます。
とかそんなことを考えながら選ぶのがストレスなので、簡単にランダムに選べる Discord bot を作りました。
出来上がったもの
アプリ
以下のリンクからサーバーにインストールすれば使えます。
使い方ですが、自分が入室しているボイスチャンネルで /test
を実行すれば一人ランダムに選ばれるはずです。コマンドが雑ですみません、 コマンドどうやって変更するのかわからなくて困ってます。助けて
実際に使ってみるとこんな感じ ↓
入室して実行、退室して実行してますが、友達がおらず一人で試した結果ぱっと見ちゃんと動いているのかわからないですね。
ソースコード
ソースコードはこちらです。欲しい機能があれば issue か PR ください。
リファクタリングの PR とかも大歓迎です。
やったこと
使用ツールなど
- フレームワーク:Hono
- 言語:TypeScript
- デプロイ先:Cloudflare Workers
ちなみに discord.js は Cloudflare だと使えないらしいので使ってないです。詳しくは以下の記事をご参考ください。
そして後から気づきましたが Hono で Discord bot を開発しやすいようにツール作っている人がいました。Hono でやろうと思っている人は是非こちらを使ってみてください。この記事で一番重要な情報はここかも知れない。 中身見てないから知らんけど。
Discord アプリ 設定
まずは Discord アプリを設定していきます。アプリと bot の使い分けがよくわかりませんが、アプリという枠の中の一つとして bot があるみたいです。
アプリ作成
ここにアクセスして New Application で適当な名前をつけてアプリを作成します。
各種設定
アプリを作成したら左サイドメニューから各種設定をしていきます。今回作りたいもの的には下記 4 つを設定すれば問題ありません。
- Installation メニューの Installation Contexts で Guild Install のみオンにする
- Installation メニューの Default Install Settings で Guild Install に以下を追加する
- SCOPES:
bot
- PERMISSIONS:
Send Messages
- SCOPES:
- Bot メニューの Privileged Gateway Intents で Server Members Intent をオンにする
- (コードのデプロイ後)General Information メニューの Interactions Endpoint URL にデプロイした URL を入力する
それぞれ簡単に説明しておきますが、詳しくはググったり生成 AI 様に聞いてください。
- Installation メニューの Installation Contexts で Guild Install のみオンにする
- Discord アプリはユーザーにインストールするか、サーバーにインストールするかを選ぶことができます。ユーザーにインストールすると、ユーザーだけがそのアプリを使えるようになります。これは特別な権限など必要なくできますが、サーバー時の情報を取得できないので、今回のように「このサーバーのこのチャンネルに入っている人を取得する」みたいなタスクは出来ません。そのためユーザーインストールをオフにしておきます。
- Installation メニューの Default Install Settings で Guild Install に以下を追加する
- bot に対してメッセージ送信の権限を追加しています。こうすることでこのアプリ(に連携したコード)がメッセージ送信をできるようになります。
- Installation メニューの Default Install Settings で Guild Install に以下を追加する
- インテントという概念が急に出てきて割と意味不明ポイントなのですが、「bot が受信可能なイベント」という意味らしく、この
Server Members Intent
をオンにすることで、サーバへの入退室情報などが取得できるようになります。これでボイスチャンネルにいる人を取得できるようになります。
- インテントという概念が急に出てきて割と意味不明ポイントなのですが、「bot が受信可能なイベント」という意味らしく、この
- (コードのデプロイ後)General Information メニューの Interactions Endpoint URL にデプロイした URL を入力する
- Discord 上でコマンドを実行した時に POST で叩かれる URL を設定します。ここは Discord の Verify とかあるのでデプロイした後じゃないと設定できません。公式ドキュメントでは ngrok 使ってテスト的に設定する方法が記述されていますが、現在は ngrok の仕様が変わって課金しないと設定ができません。
コーディング
次にコーディングしていきます。処理の流れは以下の通りです。
- Discord bot 用の検証
- コマンドが実行されたサーバーとチャンネル情報を取得する
- サーバーに所属している全ユーザー ID を取得する
- 各ユーザーに対して、対象ボイスチャンネルに入室しているかを確認する
- 入室していたユーザーの中から一人ランダムに抽出する
検証と 各情報の取得方法について簡単に説明します。
Discord bot 用の検証
メインの処理を実行する前に、Discord からコマンドを実行されて届いたリクエストであることを検証する必要があります。この処理を入れないと Discord アプリに処理を登録できないので必須です。
実際にこの bot で書いたコードは以下の通りです。
app.use("/interactions", async (c, next) => {
const { DISCORD_PUBLIC_KEY } = env<{ DISCORD_PUBLIC_KEY: string }>(c);
if (!DISCORD_PUBLIC_KEY) {
return c.json({ message: "DISCORD_PUBLIC_KEY is not set" }, 500);
}
const signature = c.req.header("X-Signature-Ed25519");
const timestamp = c.req.header("X-Signature-Timestamp");
if (!signature || !timestamp) {
return c.json({ message: "invalid request signature" }, 401);
}
const rawBody = await c.req.raw.clone().text();
const isValidRequest = await verifyKey(rawBody, signature, timestamp, DISCORD_PUBLIC_KEY);
if (!isValidRequest) {
return c.json({ message: "invalid request signature" }, 401);
}
const body = JSON.parse(rawBody);
if (body.type === InteractionResponseType.PONG) {
return c.json({ type: InteractionResponseType.PONG });
}
await next();
});
処理としては、環境変数から Discord public key を取得して、リクエストヘッダから必要な情報を取得して、真ん中くらいにある verifyKey で検証をしています。verifyKey は discord-interaction で実装されているメソッドです。
最後の方に以下のコードがありますが、これは Discord アプリに Endpoint を設定する際に、Discord が検証用コードを検証するために送ってくるリクエストを捌いています。
if (body.type === InteractionResponseType.PONG) {
return c.json({ type: InteractionResponseType.PONG });
}
詳しくは公式ドキュメントに記載がありますので読んでください。
When adding your Interactions Endpoint URL, Discord will send a POST request with a PING payload with a type: 1 to your endpoint. Your app is expected to acknowledge the request by returning a 200 response with a PONG payload (which has the same type: 1). Details about interaction responses are in the Receiving and Responding documentation.
https://discord.com/developers/docs/interactions/overview#configuring-an-interactions-endpoint-url
各種情報の取得方法
今回、サーバー、チャンネル、ユーザー情報をそれぞれ以下の方法で取得しています。
- サーバー、チャンネル:リクエストボディから取得
- サーバーに所属するユーザー:API (
https://discord.com/api/v10/guilds/${guildId}/members?limit=100
)から取得 - 各ユーザーが入室しているチャンネル:API(
https://discord.com/api/v10/guilds/${guildId}/voice-states/${userId}
)から取得
ここで面倒なのが、2 点ありました。
- サーバーに所属するユーザー情報を取る API ですが、limit のデフォルトが 1 になっており、何回やっても何回やっても一人しか取得できなくて時間を溶かしました。
- サーバーに所属するユーザー情報を取得しただけでは、ユーザーがボイスチャンネルに入室しているかの情報が取れないため、各ユーザーが入室しているチャンネル情報をループで個別に取得しています。面倒ですね。
改良したい点
- コードが雑
- わざわざユーザー名を取得しなくてもユーザー ID だけでメンションとして飛ばせるっぽいのでユーザー名取得する処理を消したい
- デフォルト?の
/test
コマンドをやめたい - 全員をランダムに並べるとかもやりたい
おわりに
久々にブログ書いたけど情報の整理が難しすぎて辛かったです。わけわからんとこ多いと思うので気軽にコメントください。あと実際にアプリ使った感想とかもあると嬉しいです。面白かった方はチャンネル登録・高評価よろしくお願いします。
Discussion