DiscordBotをCloudflareWorkers + PlanetScale + Auth0 + Next で作ったら開発体験が良す

2023/02/14に公開

ぎた(字余り)

題名の通り色々やっていくうちに躓きポイントがたくさんあったのでその共有です
特に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を作る

作りましょう
https://discord.com/developers/applications

作ったらメニューの OAuth2 -> URLGenerator から bot|application.commandsSendMessages|UseSlashCommands をチェック入れてURLを作ります
作ったURLに自分で飛んで、適当なサーバーに追加したらBotがサーバーに追加されます
必要に応じてRoleは追加しましょう いつでもURL作れるので必要になったタイミングでOKです

Wrangler に上げるコードを作る

実はDiscordの公式ドキュメントにCloudflareWorkers用の作り方が書いてあるので基本この通り作ればOKです
https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers

注意点は、 Interaction Endpoint URL をDiscordのポータルにセットする時に、ここまでにDiscordから受けたリクエストを検証するコードを書いておく必要があることです
https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers

これはドキュメントと順番が逆なので注意してください

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を使ったので↓のサイトを参考に設定しました
https://auth0.com/docs/quickstart/webapp/nextjs/01-login

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リクエストを送信することができます
https://auth0.com/docs/api/management/v2

実はこれにもアクセストークンが必要です OAuth0にトークンをもらうリクエストを送ります
https://auth0.com/docs/secure/tokens/access-tokens/get-management-api-access-tokens-for-production

はい、ここでそのままリクエストして、トークンはもらえてもほとんど権限をもってません
先程言っていたOAuth2のトークンを取ってくるためのスコープを設定する必要があります
Discordのトークンを入手するには、 GetUsersById を叩く必要がありますが、
これには read:usersread:user_idp_tokens が必要になります
https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id

このスコープの付与には CreateGrants のAPIを叩きます
https://auth0.com/docs/api/management/v2#!/Client_Grants/post_client_grants

この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のアクセストークンを取得できるはずです

サーバーのコードでやることは以下です

  1. 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/' を使います
  2. 取ってきたデータ res.data.identities から connection === "discord" を探して accessToken を取得する
  3. これをつかって Discord にリクエストする

全体の流れとしてはこんな感じです

Discord からデータを取得する

注意点だけ書きます

  • discord.jsnew REST().setToken(?) はOAuth2アクセストークンを入れても動かないので、素直に fetch('https://discord.com/api/v10/**', { headers: { Authorization: Bearer ??? } } ) にする
  • /guilds/{guildId}guilds じゃ取れなくて、取れるのは /users/@me/guilds です

良かったところ

Wrangler の開発体験が良い

Cloudflare が提供してる Wrangler の開発体験がめちゃくちゃいいです
特に何も考えずに wrangler loginwrangler init, wrangler dev を順番に叩くだけで勝手に開発サーバーができます
デプロイするときも wrangler publish するだけです かなり今風のCLIですね

DiscordBot の開発に変な制限とか無い

事前に申請が必要だとかそういうのは無いのですぐに作り始められます
一応100サーバー以上の使用は何かしらの承認してもらうとか必要みたいです この辺はエアプなのでわかりません

PlanetScale のCLIのプロキシが使いやすい

pscale connect AAA BBB --port ****
https://planetscale.com/docs/reference/connect
DB情報をローカルに記載しなくて良くてすごい良いです

感想

初めてCloudflareWorkesもDiscordBotもPlanetScaleもAuth0も使いましたが全部良く出来ててめちゃくちゃ良かったです
こうして文章にすると覚えないといけないことが多いように感じますが、特に認証のコードを書かなかったことで相当時短してますしどっこいでしょうか
Auth0+OAuth2やCloudflareWorkesなどは色々なことに使えそうなのでやっておいて良かったです

皆さんも是非やってみてください

Discussion