Closed4

DiscordのSlash Commandをwebhookで受信する方式で作ってみる

azechiazechi

スラッシュコマンドについて

Documentation Slash Commands

  • public developer beta だよ
  • コマンドはアプリケーションに登録する
    • コマンドの登録にはapplications.commands.updateスコープの承認が必要
  • コマンドは {名前, description, optionブロック} で構成される
    • optionはユーザー入力を検証する
  • コマンドはアプリケーションが追加されたギルドで使用できる
    • アプリケーションはギルドにapplications.commandsスコープの承認を持つ必要がある
      • ギルドの管理者権限を持つユーザーにブラウザで承認要求URLにアクセスさせ承認を得る
        • 承認要求ダイアログでそのユーザーが管理者権限を持つギルドのリストからアプリケーションを追加するギルドを選択する
    • ギルドに追加されたアプリケーションはDiscordクライアントに表示される
      • 「サーバー設定」->「連携サービス」->「Botおよびアプリ」に表示される
    • ギルドにBotユーザーを追加せずにアプリケーションとやりとりができる
  • アプリケーションは特定のギルドでのみ有効なコマンドも持つことができる
  • ユーザーがコマンドを使用するとアプリケーションはメッセージを受け取る
    • メッセージにはguild_id, channel_id, memberなどのメタデータが付加される

interaction : やりとり、interaction id

  • ユーザーがコマンドを使うとinteractionが開始する
  • アプリケーションはinteractionを受け取り、応答を返す
azechiazechi

webhookでinteractionを受信する

アプリケーションにInteraction Endpoint URLを設定すればwebhookが有効になる。webhookが有効になったらgatewayにはinteractionのイベントは送信されなくなる。endpoint urlを設定するときにはdiscordから確認のping が送信されるので、pong応答をすること。
webhookのHTTPリクエストは署名の検証をすること。失敗したら401を返すこと。定期的にセキュリティチェックが実施される(検証に失敗するリクエストが送られるのかな)、ちゃんと検証してなかったらアプリケーションのendpoint url が削除される。

interactionへの返信もwebhook。なので、botでのメッセージ送信と適用される制限が違うので注意。

  • 1つのinteractionへ複数の返信が可能
  • 送信した返信を編集、削除することが可能
  • interactionへの最初の返信(initial response)は3秒以内
  • interactionの有効期間は15分
    • なのでこの時間内であれば追加の返信、送信済みの返信の編集と削除が可能
azechiazechi

アプリケーションにコマンドを登録する

コマンド登録HTTPエンドポイントへコマンド{name, description, options}をPOSTすればコマンドが登録できる。このリクエストにはアプリケーションのapplicaitons.commands.updateスコープのClient Credentials Tokenが必要。
https://discord.com/developers/docs/interactions/slash-commands#registering-a-command

client credentials tokenの取得方法はこちら。docsのコード例はpython。
https://discord.com/developers/docs/topics/oauth2#client-credentials-grant

import requests
import base64
 
CLIENT_ID = ''
CLIENT_SECRET = ''

APPLICATION_ID = ''
GUILD_ID = ''
 
def get_token():
  data = {
    'grant_type': 'client_credentials',
    'scope': 'applications.commands.update'
  }
  headers = {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
r = requests.post('https://discord.com/api/v8/oauth2/token', data=data, headers=headers, auth=(CLIENT_ID, CLIENT_SECRET))
r.raise_for_status()
return r.json()['access_token']
 
url = f"https://discord.com/api/v8/applications/{APPLICATION_ID}/guilds/{GUILD_ID}/commands"
 
json = {
  "name": "blep",
  "description": "Send a random adorable animal photo",
  "options": [
    {
      "name": "animal",
      "description": "The type of animal",
      "type": 3,
      "required": True,
      "choices" : [
        {
          "name": "Dog",
          "value": "animal_dog"
        },{
          "name": "Cat",
          "value": "animal_cat"
        },{
          "name": "Penguin",
          "value": "animal_penguin"
        }
      ]
    },
    {
      "name": "only_smol",
      "description": "Whether to show only baby animals",
      "type": 5,
      "required": False
    }
  ]
}

headers = {
  "authorization": "Bearer " + get_token()
}

r = requests.post(url, headers=headers, json=json)
azechiazechi

Interaction Endpointを用意する

discordからのwebhookを受け取るHTTPエンドポイントを実装する。
Ed25519の鍵での署名を検証する必要がある。

どこに作るか
  • Azure API Managementならポリシー式で署名の検証が出来そう
    • 現在のポリシー式では出来ないっぽい、EdDSAに対応してない
とりあえず、GCP Functions の認証なしHTTPトリガーでやってみる
  • 課金が有効なGCPプロジェクト
  • プロジェクトで functionsとbuildのAPIが有効
gcloud functions deploy commands --runtime nodejs14 --trigger-http --allow-unauthenticated --set-env-vars CLIENT_PUBLIC_KEY=<discord app の public key>
discord提供のwebhook受信用のjsライブラリ

署名の検証関数と定数の定義が提供される
https://github.com/discord/discord-interactions-js

署名を検証して内容がコマンドでなければPONG応答を返す。

// https://github.com/discord/discord-interactions-js/blob/main/examples/gcloud_function.js
// 使用していない変数を削除した

const { InteractionResponseType, InteractionType, verifyKey } = require('discord-interactions')
 
const CLIENT_PUBLIC_KEY = process.env.CLIENT_PUBLIC_KEY;
 
exports.commands = async (req, res) => {

  const sig = req.get('X-Signature-Ed25519');
  const time = req.get('X-Signature-Timestamp');
  const isValid = await verifyKey(req.rawBody, sig, time, CLIENT_PUBLIC_KEY);

  if (!isValid) {
    return res.status(401).send('invalid request signature');
  }
 
  const interaction = req.body;
  if(interaction && interaction.type === InteractionType.COMMAND) {
    res.send({
      type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
      data: {
        content: `You used: ${interaction.data.name}`
      },
    });
  } else {
    res.send({
      type: InteractionResponseType.PONG,
    });
  }

};
このスクラップは2021/11/28にクローズされました