DiscordBotをCloudflareWorkers + PlanetScale + Auth0 + Next で作ったら開発体験が良す
ぎた(字余り)
題名の通り色々やっていくうちに躓きポイントがたくさんあったのでその共有です
特にCloudflareWorkersにだけ留意しておけば結構すんなり作れると思います
DiscordBot に限らず他の用途にも使えそう
注意するところ
CloudflareWorkers は Node.js のAPIが使えない
これが割りと難しいところです Node.jsのAPIに依存していないライブラリなどを選定する必要があります
ただ、謹製で fetch
とかは使えるので過不足はないかと思います
ルーティングも無く export default { fetch() }
の1関数のみで受け付けるため、それでは流石に不便ということで今回は itty-router
を使って簡単にルーティングを付けました
この辺の薄さを最初に理解しておく必要があります
Prisma はそのまま使えない
Prismaは真っ先に使いたくなりますが、使えません 一応 Prisma DataPlatform を使えばできますが、 ping が高すぎて使い物になりません
諦めて CloudflareD1 を使うか、KVで我慢するか、他の選択肢を見つける必要があります
今回は https://www.npmjs.com/package/@planetscale/database パッケージが対応していたためPlanetScaleを使うことにしました
正直僕は個人開発でPlanetScaleはブランチが邪魔なので必要ないと思います
あとは、データベースマイグレーションはSQLファイルを書きたくなかったので、そこだけPrismaSchemaを作って prisma db push
でやりました
OAuth2は認可のためにあり、認証はできない
こういうBot系の制作にありがちですが、ユーザーの認証はOAuth2ではできません
Discordの情報を返すだけとか、BOTだけの提供なら大丈夫なんですが、自分のWebアプリケーションで情報を持つ場合はそれらが丸見えになってしまいます マジで注意してください
詳しくはたくさん記事を書かれている先輩方がいらっしゃるので調べてみてください……
なので別口で認証するための方法を用意する必要があります
自分で作ってもいいんですがセッション管理は地味に面倒なので今回はAuth0を使いました
DiscordのOAuth2をAuth0に繋いで、Identityの管理とログインセッションの管理をAuth0にやってもらいます
めちゃくちゃ楽です FirebaseAuthenticatorよりも断然使いやすいので後ほど詳しく説明します
開発の流れ
簡単な流れと躓いたところや注意点を書き連ねておきます
Discord Developer ポータルからBotを作る
作りましょう
作ったらメニューの OAuth2 -> URLGenerator
から bot|application.commands
と SendMessages|UseSlashCommands
をチェック入れてURLを作ります
作ったURLに自分で飛んで、適当なサーバーに追加したらBotがサーバーに追加されます
必要に応じてRoleは追加しましょう いつでもURL作れるので必要になったタイミングでOKです
Wrangler に上げるコードを作る
実はDiscordの公式ドキュメントにCloudflareWorkers用の作り方が書いてあるので基本この通り作ればOKです
注意点は、 Interaction Endpoint URL
をDiscordのポータルにセットする時に、ここまでにDiscordから受けたリクエストを検証するコードを書いておく必要があることです
これはドキュメントと順番が逆なので注意してください
PlanetScale を準備する
必ず @planetscale/database
パッケージを使います
DBの宛先情報をPlanetScaleから取る時はダッシュボードトップの connect
ボタンを押して、 Connect With
から @planetscale/database
を選びます
よくある DATABASE_URL
のような単一のURLでは接続できず、ホストとユーザー名とパスワードが別々で必要みたいなので注意です
wrangler publish してDiscordポータルのURLを書き換える
デプロイしたらCloudflareWorkersのダッシュボードからURLを確認して、 Interaction Endpoint URL
を書き換えましょう
管理用のWebを作る
Botのコマンドを受けるだけじゃなくて管理画面を作る場合はこれに加えて Auth0 とか Nextjs とかをセッティングします
Auth0 のテナントを作ってDiscordと繋げる
Auth0 のテナントを作ったら Authentication > Social
からGoogleは消して、Discordを追加します
が、アドオンで用意されてるDiscordのやつはOAuth2のスコープを自由にいじれず identity|email
固定なのでプラスで欲しい人は自分で作りましょう
僕は今回 guilds guilds.members.read
などが欲しかったので自分で作りました
設定値はこんな感じにします scope は自由に OAuth2Generator のチェックボックスなどを見ながら自由に弄ってください
Fetch User Script
はこんな感じにしてます コピペしてOKです
function(accessToken, ctx, cb) {
request.get(
{
url: 'https://discord.com/api/v10/users/@me',
headers: {
Authorization: 'Bearer ' + accessToken,
},
},
(err, resp, body) => {
if (resp.statusCode !== 200) {
cb(new Error(body)) ;
}
const bodyParsed = JSON.parse(body);
cb(null, {
name: bodyParsed.username,
nickname: bodyParsed.username,
email: bodyParsed.email,
user_id: bodyParsed.id,
picture: bodyParsed.avatar ? 'https://cdn.discordapp.com/avatars/' + bodyParsed.id + '/' + bodyParsed.avatar + '.png' : ''
});
}
);
}
Auth0のSDKを使って認証する
今回はNext.jsを使ったので↓のサイトを参考に設定しました
NextからDiscordのIDをAuth0のIDTokenから取れるようにする
Auth0 のSDKでは session.user
という名前でユーザー情報にアクセスできますが、当たり前ですが「このAuth0のユーザーはどのDiscordユーザーに対応している」という情報は含まれてません
これはAuth0のSDKが idToken
をパースしたClaimを session.user
に丸投げしているためです
ということはDiscordを使ってログインした時に idToken
へDiscordIDをセットしてしまえばいつでも idToken
から取り出せて便利!ということで、Auth0のダッシュボードからそういった設定を簡単に作れます
AuthPipeline > Rules
からルールを作ります ↓のやつをコピペすればいいのでテンプレートも適当に選んでOKです
function putDiscrdIdIntoIdToken(user, context, callback) {
if (context.connection !== 'discord') {
return callback(null, user, context);
}
const _ = require('lodash');
const discordIdentity = _.find(user.identities, { connection: 'discord' });
if (!discordIdentity) {
return callback(null, user, context);
}
context.idToken = {
...context.idToken,
discord_id: discordIdentity.user_id.replace('discord|', '')
};
return callback(null, user, context);
}
ログインしたときの user.identities
にDiscordのOAuth0認証情報が入ってるのでそこから取ってきています
注意してほしいのは、確かできなかったと思いますが、間違っても idToken
にアクセストークン入れちゃだめです
NextからDiscordAPIにアクセスしたいが、アクセストークンが無い
idToken にアクセストークン入れるのは危険(多分)な気がするので、Auth0API経由でDiscordのアクセストークンを取得します
これはOAuth2の認可が出た時に発行されるもので、Auth0のログインをした時のトークンです
このトークンを使って取得するデータはOAuth0の Social Connections の設定の scope 依存になります
OAuth0はManagementAPIというものがあり、自分のOAuth0アプリケーションに対してAPIリクエストを送信することができます
実はこれにもアクセストークンが必要です OAuth0にトークンをもらうリクエストを送ります
はい、ここでそのままリクエストして、トークンはもらえてもほとんど権限をもってません
先程言っていたOAuth2のトークンを取ってくるためのスコープを設定する必要があります
Discordのトークンを入手するには、 GetUsersById を叩く必要がありますが、
これには read:users
と read:user_idp_tokens
が必要になります
このスコープの付与には CreateGrants のAPIを叩きます
このAPIを叩く時は開発用のスーパー権限トークンを使います
Auth0のダッシュボードから Applications > APIs
のManagementAPIとか書いてあるところに行って、APIExplorerのタブに行くとトークンが書いてあります
これを使ってCreateGrantsを叩きます この作業1回のみで大丈夫です
Bodyはこんな感じです
{
"client_id": "{作ったApplicationのID}",
"audience": "{作ったApplicationのURL}/api/v2/",
"scope": [
"read:users",
"read:user_idp_tokens"
]
}
ここまでできれば問題なくAuth0の GetUserById からDiscordのアクセストークンを取得できるはずです
サーバーのコードでやることは以下です
- Auth0のAPI
/users/${session.user.sub}
を叩く-
grant_type: "client_credentials"
client_id: process.env.AUTH0_CLIENT_ID
client_secret: process.env.AUTH0_CLIENT_SECRET
audience: process.env.AUTH0_ISSUER_BASE_URL + '/api/v2/'
を使います
-
- 取ってきたデータ
res.data.identities
からconnection === "discord"
を探してaccessToken
を取得する - これをつかって Discord にリクエストする
全体の流れとしてはこんな感じです
Discord からデータを取得する
注意点だけ書きます
-
discord.js
のnew REST().setToken(?)
はOAuth2アクセストークンを入れても動かないので、素直にfetch('https://discord.com/api/v10/**', { headers: { Authorization:
Bearer ???} } )
にする -
/guilds/{guildId}
はguilds
じゃ取れなくて、取れるのは/users/@me/guilds
です
良かったところ
Wrangler の開発体験が良い
Cloudflare が提供してる Wrangler の開発体験がめちゃくちゃいいです
特に何も考えずに wrangler login
と wrangler init
, wrangler dev
を順番に叩くだけで勝手に開発サーバーができます
デプロイするときも wrangler publish
するだけです かなり今風のCLIですね
DiscordBot の開発に変な制限とか無い
事前に申請が必要だとかそういうのは無いのですぐに作り始められます
一応100サーバー以上の使用は何かしらの承認してもらうとか必要みたいです この辺はエアプなのでわかりません
PlanetScale のCLIのプロキシが使いやすい
pscale connect AAA BBB --port ****
DB情報をローカルに記載しなくて良くてすごい良いです
感想
初めてCloudflareWorkesもDiscordBotもPlanetScaleもAuth0も使いましたが全部良く出来ててめちゃくちゃ良かったです
こうして文章にすると覚えないといけないことが多いように感じますが、特に認証のコードを書かなかったことで相当時短してますしどっこいでしょうか
Auth0+OAuth2やCloudflareWorkesなどは色々なことに使えそうなのでやっておいて良かったです
皆さんも是非やってみてください
Discussion