Discord のコマンド (Application Commands) を AWS CDK + AWS Lambda で実装してみた

2022.11.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

メッセージアプリの Discord のスラッシュコマンド (Application Commands) は、Webhook でも実装できるようになっています。
今回は AWS CDK + AWS Lambda (Function URL) で実装してみました。

参考:

注意点

Webhook で実装する際の注意として、Gateway Events は使用できません。

Gateway の機能を使う場合は Gateway にコネクションを貼り続ける必要があるので、Lambda では実装できません。
このような場合は従来の常駐型 Bot のほうが適しています。

Discord に Application を登録する

コマンドを作るためには、Discord に Application を登録する必要があります。

Discord Developers Portal のアプリケーション一覧にアクセスします。

New Application をクリックします。 New Applicationをクリック

用途がわかる適当な名前を入力して、Create を押します。 アプリ名を入れてCreate

Bot の作成

ユーザーにコマンドの応答を返す Bot を実装します。
Bot ユーザーを作成するには、Bot メニューを開きます。
アプリ設定メニューから Bot を選択します

Add Bot をクリックします。
Add Bot をクリック

Bot ユーザーを追加していいかどうかの確認画面がでるので Yes, do it! をクリックします。

cdk init する

cdk init します。
今回は TypeScript で書くので

npx cdk init --language typescript

とします。

CDK スタックを記述する

以下のような形でスタックを用意します:

lib/my-stack.ts

export class MyStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const applicationCommandHandler = new NodejsFunction(
      this,
      "application-command-handler",
      {
        entry: "./src/index.ts",
        handler: "handler",
        environment: {
          DISCORD_PUBLIC_KEY: "Developers PortalからコピーしたPublic Key"
        }
      }
    );
    applicationCommandHandler.addFunctionUrl({
      authType: FunctionUrlAuthType.NONE,
    });
  }
}

Developers Portal のアプリケーション設定から Public Key をコピーして、DISCORD_PUBLIC_KEY 環境変数の値を編集します。
Public Keyをコピーします

コマンドの登録処理を書く

コマンドの登録には Bot のアクセストークンが必要です。

アクセストークンをコピーするために、Reset Token でトークンをリセットします。

アクセストークンをコピーします。今後二度と表示されないので安全な場所にメモしておきます。
Discord へコマンドを登録する API を呼び出す際に必要になります。

グローバルコマンドは /applications/{applicationId}/commands に POST することで登録できます。
以下のようなスクリプトを用意しておくと簡単です:

scripts/register-commands.mjs

const commands = [
  {
    name: "hello", // コマンド
    type: 1, // CHAT_INPUT
    description: "Lambda で実装したコマンド", // コマンドの説明
  },
];

await fetch(
  `https://discord.com/api/v8/applications/${process.env["APPLICATION_ID"]}/commands`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bot ${process.env["BOT_ACCESS_TOKEN"]}`,
    },
    body: JSON.stringify(commands),
  }
);
APPLICATION_ID=アプリケーションID BOT_ACCESS_TOKEN=アクセストークン node scripts/register-commands.mjs

Application Command Object の詳細:

コマンド登録処理の詳細:

Webhook ハンドラとなる Lambda 関数を書く

依存関係をインストールします。

npm install aws-lambda @types/aws-lambda discord-interactions

Lambda ハンドラーを記述します。

src/index.ts

import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
import {
  InteractionResponseType,
  InteractionType,
  verifyKey,
} from "discord-interactions";

// Interaction リクエストの署名検証 (ないと失敗する)
const verifyRequest = (event: APIGatewayProxyEventV2) => {
  const { headers, body } = event;
  const signature = headers["x-signature-ed25519"];
  const timestamp = headers["x-signature-timestamp"];
  const publicKey = process.env["DISCORD_PUBLIC_KEY"];
  if (!body || !signature || !timestamp || !publicKey) {
    return false;
  }
  return verifyKey(body, signature, timestamp, publicKey);
};

// interaction の処理
const handleInteraction = (interaction: Record<string, unknown>) => {
  if (interaction.type === InteractionType.APPLICATION_COMMAND) {
    const { data } = interaction as { data: Record<string, unknown> };
    if (data.name === "hello") {
      return {
        type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
        data: {
          content: "hello, world",
        },
      };
    }
  }

  return { type: InteractionResponseType.PONG };
};

// エントリポイント
export const handler = async (
  event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
  if (!verifyRequest(event)) {
    return {
      statusCode: 400,
    };
  }

  const { body } = event;
  const interaction = JSON.parse(body!);

  return {
    statusCode: 200,
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(handleInteraction(interaction)),
  };
};

実行されたら hello, world と返すだけのコマンドを実装しています。

デプロイ

デプロイします。

npx cdk bootstrap
npx cdk deploy

Interaction URL を設定する

デプロイが完了したら、Interaction Endpoint URL にデプロイした Lambda 関数の URL をセットします。

サーバーに Bot を追加する

OAuth2 メニュー → URL Generator に移動して、bot と applications.commands にチェックを入れます。
Discord Bot の権限設定

一番下の Generated URL をコピーしてブラウザでアクセスします。

コマンドを実行してみる

まとめ

Discord の Interactions Endpoint 機能を活用すれば、サーバーに Bot を常駐させることなく実装することができます。
簡単なコマンドであればサーバーレスに実装できるので、ぜひお試しください。