Google Cloud Functionsで DiscordコマンドBotをサーバレスにする

2022-11-09

はじめに

discord.py製のBotを運用していたのですが、Botを置いていたHerokuの無料枠が消えたため、Cloud Functionsに引っ越し、というかほぼ置き換えすることにしました。インターネット上に見当たらなかったのでそのメモです。
参考までに、Cloud Functionsの料金は200万リクエスト(≒コマンド呼び出し回数)まで無料です。



Firebaseに登録する

【Firebase】Cloud Functions チュートリアル
こちらの冒頭無料部分を参考に環境構築を行います。(丸投げ)


やることダイジェスト

  • Firebaseプロジェクトの作成
  • Firebaseのプランをアップグレード
  • Firebase CLIのインストール
  • $ firebase init
  • done!



Discord Botを登録する

https://zenn.dev/drumath2237/articles/112fd0bfa7ea4f836195#applicationの作成
こちらの記事のDiscord Slash Commandを作成するSlash Commandの登録までを進めます。(丸投げ)


やることダイジェスト

  • Applicationの作成
  • Slash Commandの登録
  • done!



Functionの実装・デプロイ

function/index.tsが生成されていると思うのでこれを弄っていきます。
とりあえず $ npm install discord-interactions をします。


検証について

Slash Commandのエンドポイントが不正でないか証明するために、いくつか検証プロセスが必要になります。わざと不正なリクエストが飛んできたりします。
具体的には

  • ヘッダーの署名検証を行い、不正であれば401を返す

https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorization

  • Interactionのタイプが1の場合は、PONGを返却

https://discord.com/developers/docs/interactions/receiving-and-responding#receiving-an-interaction
それを踏まえたのが以下のコードになります。今回は、コマンドに関係なく、hi! 名前と応答するよう実装します。

import * as functions from "firebase-functions";
import {
  verifyKey,
  InteractionType,
  InteractionResponseType,
} from "discord-interactions";

// Start writing Firebase Functions
// https://firebase.google.com/docs/functions/typescript

export const discordBot = functions.https.onRequest((request, response) => {
  // 検証:不正な署名を弾く
  const sig = request.headers["x-signature-ed25519"];
  const time = request.headers["x-signature-timestamp"];
  if (typeof sig !== "string" || typeof time !== "string") {
    response.status(401).send("");
    return;
  }

  const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY ?? "";
  const isValid = verifyKey(request.rawBody, sig, time, CLIENT_PUBLIC_KEY);
  if (!isValid) {
    response.status(401).send("");
    return;
  }

  const interaction = request.body;
  if (!interaction) return;
  if (interaction.type === InteractionType.PING) {
        // 検証:PINGが送信された場合はPONGを返す
    response.send({
      type: InteractionResponseType.PONG,
    });
    return;
  } else {
    // コマンド:挨拶する
    response
      .status(200)
      .type("application/json")
      .send({
        type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
        data: {
          content: `hi! ${interaction.member.user.username}`,
        },
      });
  }
});

それから、Discord ApplicationのCLIENT_PUBLIC_KEY.env ファイルに置きます。
具体的にはfunctions/.envを作成し、 Discord Developer Portalの アプリケーションの General Infomationの PUBLIC KEY を以下の様にコピペします。

CLIENT_PUBLIC_KEY=0123456789abcdef0123456789abcdef



デプロイする

$ firebase deploy します。
その後、firebaseのプロジェクトページからデプロイされた関数の詳細を確認します。
リクエストURLを控え、
Discord Developer Portalの アプリケーションの General Infomationの INTERACTION ENDPOINT に先程のリクエストURLを記述します。
お疲れ様でした。適当なサーバーでコマンドを叩いて確認してみてください。



発展

実際にコマンドを実装する

interaction.data.name でコマンド名が取れます。コマンドの判別はidでも良いでしょう。


デバッグしたい

CloudFunctionsのデバッグについては以下が参考になります。
https://qiita.com/seya/items/c37207cd65ec914692ba
DiscordBotの場合はローカルでリクエストを作るのが大変なので、公式チュートリアルも推奨しているngrokでどうにかするとよいでしょう。


おわり