Discord Slash CommandのInteractionをAzure Functionsにデプロイしてみる

2021/07/31に公開

はじめに

TL;DR

Discord の Slash Command を TypeScript を使って開発し、
Azure Functions にデプロイすることで簡単なボットのようなアプリをサーバーレスに作ることできる。

扱う内容・対象読者

この記事では、Azure Functions にデプロイした関数を
Discord Slash Command の Interaction Endpoint に指定して使うまでを目標とします。

したがって次のような項目を扱います。

  • Slash Command の概要と作成方法
  • Azure Function アプリを TypeScript で開発しデプロイする方法
  • Slash Command の Interaction を扱う方法

次のような読者層を想定して執筆しました。

  • GitHub を使っている
  • Discord を使っていて Bot を作ってみたい/作ったことがある
  • TypeScript がある程度わかる
  • Azure アカウントを持っていて、何かサービスを作ってみたい
    • (任意)課金が生じるので Azure Student のような特典を受けていると尚良いです

ただし技術的難易度は低いので、興味さえあればこの限りではございません。

環境について

今回自分が使用した環境を次に示します。

項目 環境
エディタ VSCode
Node.js v14.16.1
OS Windows 10 Home

また、VSCode で開発する際に「Azure Functions」という
Microsoft が出している拡張機能を入れているので、そちらも導入しておくことをお勧めします。

Discord Slash Commandを作成する

Slash Commandとは

そもそも Slash Command とはいったいなんでしょうか。
従来から Discord では API を使うことによって Bot を作る仕組みがあり、Python や JavaScript で実装された便利なライブラリを使うことによって簡単に作成できます。
Bot を作ることによってユーザのメッセージなどにリアルタイムで応答するような仕組みを実装できましたが、それにはソケット通信を常に行っているサーバーから実装しなくてはいけなかったため、いくつも Bot を作るのはちょっと面倒でした。
自分は Python で作った Bot を Heroku にデプロイして使っていましたが、Heroku の無料枠では 5 つまでしかアプリをデプロイできないため、シンプルなボットを作るには正直コスパが悪かったです。

Slash Command は、Bot のようなユーザとインタラクションできるアプリケーションを作る新しい仕組みです。

Slash Command を使うことにより、開発者はユーザのメッセージを Webhook 的に受け取ることができたり、ユーザはコマンド名や引数のオートコンプリートを利用できるようになりました。

Slash コマンドはアプリケーションを招待した全てのサーバーおよび DM で使える Global Command と、特定のサーバーのみで使える Guild Command の 2 種類あります。
Global Command は登録してから反映されるまで 1 時間かかったりするらしいので、今回は Guild Command を作る予定です。

Applicationの作成

まずは Discord Developer Portal にアクセスして Slash Command を使用するためのアプリケーションを作成します。
「New Application」ボタンをクリックすると名前を入力する必要があります。
「zenn-slash-tutorial」という名前で作ってみました。
img
アプリケーションのページに移動すると、General Information、OAuth2、といった下に「Bot」というタブがあるのでそこへ移動し、ボットを作成しましょう。

ボットが作成出来たら OAuth2 タブに移動して権限を付与しましょう。
Slash Command を使うためには「application.command」にチェックを入れる必要があります。
img
その時に表示される URL へ移動し、Slash Command を使うためのサーバーへ招待しましょう。無事招待できたらサーバー設定から「連携サービス」の項目にアプリケーションがあるはずですのでご確認ください。
img

Slash Commandの登録

Slash Command はアプリケーションに登録することで使うことができます。
コマンドを登録するためには discrod の API エンドポイントに POST リクエストを送る必要があり、まずはそのプログラムを実行していきましょう。
公式に Web クライアントとかできてくれると嬉しいんだけどな......。

Slash Command の作成には、次の記事を参考にさせていただきました。

https://zenn.dev/tubuan/articles/discordjs-slash-commands

Slash Command 登録のためのディレクトリを作成し、
そのディレクトリで次のコマンドを実行します。

# initialize node.js project
$ yarn init -y

# install node-fetch & request
# もしかしたらrequestいらないかもしれない
$ yarn add node-fetch request

プロジェクトの中にindex.jsを作成し、次のように編集します。

index.js
const appID = "<application id>";
const guildID = "<guild id>"
const apiEndpoint = 
  `https://discord.com/api/v8/applications/${appID}/guilds/${guildID}/commands`;
const botToken = "<bot tolen>";

const commandData = {
  name: "zenntest",
  description: "command for zenn tutorial",
  options: [
    {
      name: "say",
      description: "say something",
      type: 3,
      required: true,
      choices: []
        {
          name: "Hello",
          value: "hello"
        },
        {
          name: "Goodbye",
          value: "goodbye"
        },
        {
          name: "hoge hoge",
          value: "hoge"
        },
      ],
    },
  ],
};

async function main() {
    const fetch = require("node-fetch");

  const response = await fetch(apiEndpoint, {
    method: "post",
    body: JSON.stringify(commandData),
    headers: {
      Authorization: "Bot " + botToken,
      "Content-Type": "application/json",
    },
  });
  const json = await response.json();

  console.log(json);
}
main();

Slash Command の登録には次のような情報が必要ですので、Discord の Developer Portal やサーバー設定を適宜参照してください。

変数名 説明
appID コマンドを登録するアプリケーション ID
guildID 実際にコマンドを使用するサーバーの ID
botToken アプリケーションで有効になっている Bot のトークン

main()関数の中ではcommandDataオブジェクトを Discord API に POST する処理が書かれている感じでです。
commandDataの中身を見てみましょう。

const commandData = {
  name: "zenntest",
  description: "command for zenn tutorial",
  options: [
    {
      name: "say",
      description: "say something",
      type: 3,
      required: true,
      choices: []
        {
          name: "Hello",
          value: "hello"
        },
        {
          name: "Goodbye",
          value: "goodbye"
        },
        {
          name: "hoge hoge",
          value: "hoge"
        },
      ],
    },
  ],
};

サクッとまとめるとこんな感じになります。

プロパティ 説明 実際の動作
name コマンド名 /zenntestというコマンドを使うことになります
description コマンドの説明 コマンドを打つときに表示されます
options コマンドの引数。省略可能 今回は 1 つの引数をとるコマンドになります
options.name 引数の名前です say という名前の引数を指定します
options.type 引数の型です string 型を示す 3 にしています[1]
options.required 必須な引数か 今回は必要としています
options.choices 選択肢 Hello, Goodbye, hogehoge がオートコンプリートされます

ここまでできたら、node index.jsとして実行します。
無事に登録で来たら、コマンドの情報がログ出力されるでしょう。
もし同じコマンド名でこのプログラムを実行した場合には、最後に実行した内容で上書きされます。

実際にスラッシュコマンドを使って見ると、入力内容がちゃんと補完されていることがわかるはずです。
現時点ではまだコマンドの動作を定義していないので、コマンドを使ってもエラーになってしまいます。

img

Interaction の開発

Interaction[2]とは

コマンドが登録されたアプリケーションは、ユーザから Slash Command が発行されると
受け取った情報を登録された URL に POST します。
この POST された情報のことを「Interaction」と呼び、Interaction を POST され、
適切なレスポンスを返すサーバーの URL を Interaction Endpoint としてアプリケーションに登録します。

前述したように従来の Discord Bot では常時ソケット通信用のコネクションを張ったサーバーアプリケーションが必要でしたが、
Slash Command の場合は webhook 的に動作するサーバーレス関数でも十分に対応できます。

TypeScriptプロジェクトの作成

Slash Command の Interaction を処理するために、TypeScript のプロジェクトを作成していきます。
このプロジェクトは Azure Functions にデプロイする前提で作っていくため、VSCode の Azure Functions 拡張機能を用いてプロジェクトのひな型を作ります。

Interaction Endpoint プロジェクトのためのディレクトリを切って VSCode で開き、Azure タブからプロジェクトの初期化をしていきましょう。

img

VSCode の案内に従えばプロジェクトのテンプレートが作成されます。
プロジェクトは次の仕様で作成した前提のもと進めていきます。

環境 内容
言語 TypeScript
トリガー HttpTrigger
ランタイムスタック Node.js v14

まずはプロジェクトで必要な npm モジュールをインストールしていきましょう。

# install & add dependency
$ npm install
$ npm install discord-interactions express @types/node

Interactionを処理するプログラム

Interaction を扱うため、次のような処理をするプログラムを作成します。

  1. POST で Interaction を受け取る
  2. ヘッダーの署名を検証する
  3. 不正な署名だったら 401 を返す
  4. Interaction の type が 4 だったら Content を添えてレスポンス
  5. type が 1 だったら PONG を返す

Interaction Endpoint の登録時に、署名の検証をしているか Discord から確認されます。
自分が見た限りだと正しい署名と不正な署名の ping が送信され、
正しい署名の場合は PONG を返し、不正な署名には 401 を返す処理を要求されました。
署名の検証はdiscord-interactionsで提供されているメソッドを使います。
署名の検証には POST された情報の他にアプリケーションの Public Key を使用するため、
環境変数から参照する仕組みが必要です。

ping ではないリクエストの検証に成功したら、Interaction に対応するレスポンスを作成して返します。
Interaction の内容自体はreq.body.dataの中に格納されています。次のデータは公式ドキュメント [3]から引用したものです。

{
    "type": 2,
    "token": "A_UNIQUE_TOKEN",
    "member": {
        "user": {
            "id": 53908232506183680,
            "username": "Mason",
            "avatar": "a_d5efa99b3eeaa7dd43acca82f5692432",
            "discriminator": "1337",
            "public_flags": 131141
        },
        "roles": ["539082325061836999"],
        "premium_since": null,
        "permissions": "2147483647",
        "pending": false,
        "nick": null,
        "mute": false,
        "joined_at": "2017-03-13T19:19:14.040000+00:00",
        "is_pending": false,
        "deaf": false
    },
    "id": "786008729715212338",
    "guild_id": "290926798626357999",
    "data": {
        "options": [{
            "name": "cardname",
            "value": "The Gitrog Monster"
        }],
        "name": "cardsearch",
        "id": "771825006014889984"
    },
    "channel_id": "645027906669510667"
}

ちょっとややこしいですが、options 配列には引数の配列が格納されており、
name はコマンド名、value は options.name に対応する vaue が入っています。

それでは Slash Command を送信したユーザ名に対してオウム返しをするだけの Bot を作るコードを次に示します。

index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import {
  InteractionType,
  InteractionResponseType,
  verifyKey,
} from "discord-interactions";

const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY;

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const sig = req.headers["x-signature-ed25519"];
  const time = req.headers["x-signature-timestamp"];
  const isValid = await verifyKey(req.rawBody, sig, time, CLIENT_PUBLIC_KEY);

  if (!isValid) {
    context.res = {
      status: 401,
      Headers: {},
      body: "",
    };
    return;
  }

  const interaction = req.body;
  if (interaction && interaction.type === InteractionType.APPLICATION_COMMAND) {
    const greetingWord = req.body.data.options[0].value;
    const username = req.body.member.user.username;

    context.res = {
      status: 200,
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
        data: {
          content: `${greetingWord}, ${username}`,
        },
      }),
    };
  } else {
    context.res = {
      body: JSON.stringify({
        type: InteractionResponseType.PONG,
      }),
    };
  }
};

export default httpTrigger;

Azure Functionsへデプロイ

先ほどまでの内容をリモートリポジトリへ push します。
VSCode から直接 Azure Functions のデプロイも可能ですが、後述する事情により
今回は GitHub Actions からデプロイする方法を取ります。

Azure Functionsプロジェクトの作成

Azure Portal にログインし、Node.js v14 LTS で Azure Functions のプロジェクトを作っていきます。
その際に 「プランの種類」を「App Service プランの Basic B1 プラン」またはそれ以上にすることを推奨 します。
自分が見る限りだと、Interaction はどうやらタイムアウトがシビアなようで従来課金プランで作成した時に生じるコールドスタートによってエラーになってしまう可能性があります。
正直このトラブルシュートは十分に行えておらずあまり自信がないのですが、Premium プランでホスティングした時にエラーが起きなかったことからそう予想しています。ならば Premium でホスティングするのもありですが、やってみたところ 500 円/日くらいクレジットが消費されており学生の自分は苦しかったです。サイゼで豪遊できるくらいの金額が飛んで行ってしまった......。

Azure Portalでの作業

Azure Functions を作成できたら次の 2 点を行います。

  • Application ID を環境変数に設定する
  • GitHub からの継続的デプロイを設定する

環境変数の設定

環境変数の設定は、Azure Functions プロジェクトページ>構成>アプリケーション設定>新しいアプリケーション設定ボタンから行えます。
「CLIENT_PUBLIC_KEY」という名前で、値には Duscord Developer Portal から取得できる Public Key を設定しましょう。

img

GitHubからデプロイする

GitHub からデプロイするためには、デプロイセンター>ソース>ソースを選択、で GitHub を選択し、
リポジトリやブランチを指定することでデプロイできます。
その際に GitHub Actions 用のワークフローがリポジトリに追加されます。

img

Slash CommandにInteraction Endpointを設定する

本記事の仕上げとして、Discord Developer Portal から Interaction Endpoint を設定しましょう。
アプリケーションページの General Information に「INTREACTION ENDPOINT URL」という項目があるので、
デプロイした Azure Functions の URL を指定して保存します。この時に署名がちゃんと検証されているかを確認するための ping が Discord から投げられるので、それを適切に処理できれば保存ができます。

img

それができたらサーバーからスラッシュコマンドを送信してみてください。
Bot が応答しているのが確認できたでしょうか。

img

おわりに

まとめ・感想

今回は Discord の比較的新しい機能である Slash Command に触れました。
Slash Command とサーバーレス関数を使用することで、手軽にボットアプリケーションを作成できるという体験はとても良かったと感じました。
自分はまだ Azure には無知なところがあるので、今回取り組んだことは結構勉強になって良かったです。
この内容が皆さんのお役に立てれば幸いです。最後まで読んでいただき、ありがとうございました。

参考

脚注
  1. optionの型 ↩︎

  2. Receiving an Interaction ↩︎

  3. Interactionの書式 ↩︎

GitHubで編集を提案

Discussion