😽

Discord スラッシュコマンドの引数定義(Application Command Option Type) の整理

2023/07/12に公開2

はじめに

Discordのスラッシュコマンドの開発時、コマンドの引数の定義でもたつきがちなので自分用に整理し、サンプルコードも作ってみました。

本資料でコマンドの引数と表記しているものは以下のスクショの "str" や "int"のようなパラメーターのことです。

前提・注意事項

コードは全てNode.jsで書いてますが、パラメタの構造の話なので他の言語の方でも役立つかもしれません。
2023.07.12時点の公式ドキュメントと実行結果に基づいています。

またこの資料を読まれる前に

あたりは大体の理解をしていただく必要があるかと思います。

掲載した公式資料のスクショは、文字が潰れて読めないのもあると思うので、リンク先の公式ドキュメントで直接見てください。
(あまり大きな画像貼っても重たくなりますし、全くスクショなしだと、それはそれで読みにくいので)

一番シンプルなパターン

サンプルコードのコマンド登録処理部分に渡す、コマンドの定義の一番シンプルな形は以下になります。

/test という引数無しのコマンドです。

const TEST_COMMAND = {
  name: 'test',
  description: 'Basic command',
};
const ALL_COMMANDS = [TEST_COMMAND];
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);

指定できる値の説明は以下を参考にしてください。

Application Command Object

ただし、コマンド作成時に指定できる値の説明というより、コマンド実行後作成されるオブジェクトの説明といった内容で、例えば

  • id は作成時に自動的に採番されので指定が不要なようです。
  • type は 指定しなければdefaultの1でスラッシュコマンドとなります。
  • application_id は process.env.APP_ID で指定します。
    他項目も任意の値なので、作成時に省くことができます。
    コマンド作成時のパラメタの仕様書と混同しないようにしてください。

※ ちなみに、引数定義なしで作った上記コマンドは、実行時コマンド名の後ろに値を入力できます。気になってログをログを取ってみたのですが、その値を取得することはできません。無視されているようです。なかなかドキュメントだけではわからないことが多く、実際に動かしながら理解を深める必要があります。

引数利用時

const TEST_COMMAND = {
  name: "test",
  description: "Basic command2",
  type: 1, //Application commandの種別を指定するtype
  options: [
    {
      name: "txt",
      type: 3, // スラッシュコマンドの引数の種別・型を指定するtype
      required: true,
      description: "memo",
    },
  ],
};

options を指定することで引数を定義します。これは type が 1(つまりスラッシュコマンド)の時のみ利用可能です。
Application Command Types

今回はスラッシュコマンドについてのみ扱うのでコマンドのtypeは常に 1 です。
以後主に optionsの定義の話になりますが、options内のtypeと混同しないように気をつけてください。

optionsの定義

options 内の要素の定義は以下になっています。

Application Command Option Structure

既に触れたように、同じtypeで紛らわしいですが、 options 内の要素が示す type はスラッシュコマンドの引数の型を指定するためのものです。

Application Command Option Type

上記が指定できる引数の型の一覧になります。

基本的な型 (String, Integer, Boolean, Number)

ここからはoptionsを指定して、引数を使ったコマンドを作ります。
名前の通り、説明はほぼ不要かと思います。
※IntegerとNumberの違いはNumberが小数点を使える点です。

サンプルコード

const TEST_COMMAND = {
  name: "test",
  description: "Basic test command3",
  type: 1,
  options: [
    {
      name: "str",
      type: 3,
      required: false,
      description: "string",
    },
    {
      name: "int",
      type: 4,
      required: false,
      description: "integer",
    },
    {
      name: "bool",
      type: 5,
      required: false,
      description: "boolean",
    },
    {
      name: "num",
      type: 10,
      required: false,
      description: "number",
    },
  ],
};

String、Integer、Boolean、Number、それぞれの項目に型が異なる値を入力するとエラーとなりコマンドが実行できません。

スクショの例のように、booleanは入力時に候補の値が表示され選択することができます。

値の受け取り方

ここでスラッシュコマンドを実行した時、エンドポイントに送られるパラメタについて説明しておきます。
エンドポイントに送られてくるパラメタ仕様は以下の通りです。

Application Command Data Structure

先に触れておくと、ユーザーやロールが含まれる型の場合、さらに以下の項目が含まれます。
(後述します)

Resolved Data Structure

私はAWS Lambdaを利用していますが、handler の引数 eventを下記のように扱うと上記構造の値が取得できます。

const params = JSON.parse(event.body);
console.log(parmas.data)

既に紹介した型のサンプルを以下に示します。
IDが示しているのは、コマンド作成時にコマンドに割り当てられるIDです。
※サンプルのIDは実在しないものに書き換えてます。

引数無し

{
  id: '1124225850527710000',
  name: 'test',
  type: 1
}

引数が String、Integer、Boolean、Number の例

{
  id: '1124225850527710000',
  name: 'test',
  options: [
    { name: 'str', type: 3, value: 'string' },
    { name: 'int', type: 4, value: -1 },
    { name: 'bool', type: 5, value: true },
    { name: 'num', type: 10, value: 3.14 }
  ],
  type: 1
}

SUB_COMMAND, SUB_COMMAND_GROUP

次に、少し特殊な型を紹介していきます。
これを使うとoptionsが入れ子になります。

サブコマンド、サブコマンドのグループを言葉で説明するのが難しいので、具体例と実行結果を記載します。

SUB_COMMANDサンプル

const RSS_COMMAND = {
  name: "rss",
  description: "RSSリーダー",
  options: [
    {
      name: "url",
      type: 1,
      description: "フィードのURL管理",
      // required: false, trueを指定するとエラーになるので指定できない
      // メッセージは "Required cannot be configured for this type of option"
      options: [
        {
          name: "add",
          type: 3,
          description: 'URL追加',
          required: true,
        },
        {
          name: "limit",
          type: 4,
          description: '件数を指定して一覧表示する',
          required: false,
        },
      ],
    },
    {
      name: "read",
      type: 1,
      description: "フィードに付与するラベル",
      //required: true, //trueにするとエラー
      options: [
        {
          name: "limit",
          type: 4,
          required: false,
          description: '記事の取得件数',
        },
      ],
    },
  ],
};

実行例

パラメタだけだとどのような組み合わせが実行できるのか、イメージできないので、指定できそうな値の組み合わせを指定してみました。

  • rss
    実行できない。無理やり実行しようとすると "/rss" という文字列の通常投稿になる。
  • rss url
    実行できない。必ず require: true の add サブコマンドを指定する必要がある。
  • rss url add https://google.co.jp
    実行可能
  • rss url limit 10
    実行できない。必ず require: true 指定された add サブコマンドも指定する必要がある。
  • rss url add https://googole.co.jp limit 10
    実行可能。
  • rss read
    実行可能。
  • rss read hogefuga
    指定不可。(強制的にlimitが表示される)
  • rss read limit 10
    実行可能

エンドポイントに送られてくる値例(rss url add https://googole.co.jp limit 10)

{
  id: '1127948082068340000',
  name: 'rss',
  options: [
    { name: 'url',
      options: [
        { name: 'add', type: 3, value: 'https://google.co.jp' },
        { name: 'limit', type: 4, value: 1 }
      ],
      type: 1 // 引数の型
    }
  ],
  type: 1 // コマンドの種別(スラッシュコマンドを表す)
}

インラインで送信されてきたパラメタにコメント入れています。

エンドポイントに送られてくる値例(rss read)

{
  id: '1127948082068340000',
  name: 'rss',
  options: [ { name: 'read', options: [], type: 1 } ],
  type: 1
}

オプションは存在するが指定されていないので空の配列になっています。

SUB_COMMAND_GROUPサンプル

こちらはさらにサブコマンドをネストする際に使うもののようです。
自分でも活用してみようと思ったのですが、用途が思いつかず、ChatGPTに作ってもらいました。
修正なしで1回で成功したのは驚きました。

const GRP_COMMAND = {
  name: "moderation",
  description: "ミュートやキックなどの管理",
  options: [
    {
      name: "user",
      description: "ユーザー操作",
      type: 2,
      options: [
        {
          name: "mute",
          description: "ミュート",
          type: 1,
          options: [
            {
              name: "user_id",
              description: "ユーザーID",
              type: 6,
              required: true,
            },
            {
              name: "duration",
              description: "ミュート期間(分単位)",
              type: 4,
              required: true,
            },
          ],
        },
        {
          name: "kick",
          description: "キック",
          type: 1,
          options: [
            {
              name: "user_id",
              description: "ユーザーID",
              type: 6,
              required: true,
            },
          ],
        },
      ],
    },
    {
      name: "channel",
      description: "チャンネル操作",
      type: 2,
      options: [
        {
          name: "create",
          description: "新チャンネル作成",
          type: 1,
          options: [
            {
              name: "channel_name",
              description: "チャンネル名",
              type: 3,
              required: true,
            },
            {
              name: "topic",
              description: "チャンネルトピック",
              type: 3,
              required: false,
            },
          ],
        },
      ],
    },
  ],
};

実行例

  • /moderation

  • /moderation user

  • /moderation user kick

  • /moderation user mute

  • /moderation user mute user_id @dummyuser

  • /moderation channel

  • /moderation channel create

  • /moderation channel create topic test
    上記8例は指定不可でした。強制的にサブコマンドや必須指定のパラメタ入力を求められます。

  • /moderation user mute user_id @dummyuser duration 10

  • /moderation channel create channel_name お名前チャンネル
    上記は実行可能でした。

実行結果例(/moderation user mute user_id @dummyuser duration 10)

Discordから送られてくるパラメタから抜粋します。
詳細はSUB_COMMANDサンプルも参照してください。

{
  id: '1128307843636920000',
  name: 'moderation',
  options: [
    {
      name: 'user',
      options: [
        {
	  name: 'mute',
	  options: [
	    { name: 'user_id', type: 6, value: '1114147774926181111' },
	    { name: 'duration', type: 4, value: 10 }
	  ],
	  type: 1
	}
      ],
      type: 2
    }
  ],
  resolved: {
    members: { '1114147774926181111': [Object] },
    //次項で説明するのでObjectの中身は省略します
    users: { '1114147774926181111': [Object] }
    //次項で説明するのでObjectの中身は省略します    
  },
  type: 1
}

SUB_COMMANDの例よりネストが深くなっていますが、それ以外大きな違いはありません。
"resolved"という項目が加わっていますが、これは引数の型が「6」= USER だからです。
SUB_COMMAND/SUB_COMMAND_GROUPであるかとは関係ありません。

USER, CHANNEL, ROLE, MENTIONABLE, ATTACHMENT

こちらも少し特殊な型になります。
コマンド作成時のパラメタのサンプルは以下になります。

const SP_COMMAND = {
  name: "sp",
  description: "Special test command",
  type: 1,
  options: [
    {
      name: "user",
      type: 6,
      required: false,
      description: "USERのテスト",
    },
    {
      name: "channel",
      type: 7,
      required: false,
      description: "CHANNELのテスト",
    },
    {
      name: "role",
      type: 8,
      required: false,
      description: "ROLEのテスト",
    },
    {
      name: "mentionable",
      type: 9,
      required: false,
      description: "MENTIONABLEのテスト",
    },
    {
      name: "attachment",
      type: 11,
      required: false,
      description: "ATTACHMENTのテスト",
    },
  ],
};

USER

ユーザー名、またはユーザーIDで指定できます。
表示される候補からも選択できます。

CHANNEL

チャンネル名、またはチャンネルIDを指定します。
表示される候補からも選択できます。

ROLE

ロール名、またはロールIDを指定します。
表示される候補からも選択できます。

MENTIONABLE

ユーザー、ロールなどメンション可能な対象を名前やIDで指定します。
表示される候補からも選択できます。

ATTACHMENT

添付ファイルをドラッグ&ドロップして指定します。
この項目については別途記事があるので

Discord スラッシュコマンドでファイルアップロードする

上記も参照してください。

全て値を指定した場合のエンドポイントに送られる値のサンプルです。
IDの一部や画像ファイル名などはダミーに書き換えています。
(どこまでそのまま載せても問題ないか判断つかないので書き換えが多いですが)

{
  id: '1128333206991670000',
  name: 'sp',
  options: [
    { name: 'user', type: 6, value: '1114147774926181111' },
    { name: 'channel', type: 7, value: '1109741823540352222' },
    { name: 'role', type: 8, value: '1109741823540353333' },
    { name: 'mentionable', type: 9, value: '1109741823540353333' },
    { name: 'attachment', type: 11, value: '1128605894771484444' }
  ],
  resolved: {
    attachments: {
      '1128605894771484444':
      {
        content_type: 'image/png',
        ephemeral: true,
        filename: '2023-07-12_16.13.02.png',
        height: 353,
        id: '1128605894771484444',
        proxy_url: 'https://media.discordapp.net/略/略.png',
        size: 146799,
        url: 'https://cdn.discordapp.com/略/略',
        width: 800
      }
    },
    channels: {
      '1109741823540352222': {
        flags: 0,
        guild_id: '1109741823540353333',
        id: '1109741823540352222',
        last_message_id: '1127831209838381212',
        last_pin_timestamp: '2023-06-10T07:57:49+00:00',
        name: '一般',
        nsfw: false,
        parent_id: '1109741823540353434',
        permissions: '略(数値)',
        position: 4,
        rate_limit_per_user: 0,
        topic: 'チャンネルトピック',
        type: 0
      }
    },
    members: {
      '1114147774926181111': {
        avatar: null,
        communication_disabled_until: null,
        flags: 0,
        joined_at: '2023-06-02T11:27:32.501000+00:00',
        nick: null,
        pending: false,
        permissions: '略(数値)',
        premium_since: null,
        roles: ['1114153324032308888'],
        unusual_dm_activity_until: null
      }
    },
    roles: {
      '1109741823540353333': 
      {
        color: 0,
        description: null,
        flags: 0,
        hoist: false,
        icon: null,
        id: '1109741823540353333',
        managed: false,
        mentionable: false,
        name: '@everyone',
        permissions: '略(数値)',
        position: 0,
        unicode_emoji: null
      }
    },
    users: {
      '1114147774926181111': 
      {
        avatar: null,
        avatar_decoration: null,
        bot: true,
        discriminator: 略(数値),
        global_name: null,
        id: '1114147774926181111',
        public_flags: 略(数値),
        username: 'hogeuser'
      }
    }
  },
  type: 1
}

choices

HTMLで言うところのプルダウンのように、コマンド作成時に定義したリストから値を選択させることができます。

※DBなどから取得した値を動的に選択させるようなことはできません。(コマンド作成時に指定したもので固定)
(2023.07.12追記)上記斜線部分、コメント欄にて指摘をいただきました。詳細はコメント欄確認ください。

Application Command Option Choice Structure

サンプルコード

const CHOICE_COMMAND = {
  name: "choice",
  description: "Basic test command",
  type: 1,
  options: [
    {
      choices: [
        { name: "ON", value: 1 },
        { name: "OFF", value: 0 },
      ],
      name: "switch",
      type: 4,
      required: true,
      description: "on or off",
    },
  ],
};

上記のように指定した型(type)と choicesの選択肢の要素の型は一致する必要があります。
また、 MENTIONABLE のように既に既存の値から候補を表示する型の場合、choicesを使っても、選択肢が正しく表示されませんでした。作成する際はエラーは出なかったのですが。

エンドポイントに送られてくる値のサンプルは以下になります。

{
  id: '1127964967186990000',
  name: 'choice',
  options: [ { name: 'switch', type: 4, value: 1 } ],
  type: 1
}

choices を使わなかった場合と特に違いはありません。

なお、以下のサンプルのように、"autocomplete"を有効にすると、エンドポイント経由で選択肢を動的に取得する機能を有効にできます。

const CHOICE_COMMAND = {
  name: "choice",
  description: "Basic test command",
  type: 1,
  options: [
    {
      choices: [
        { name: "ON", value: 1 },
        { name: "OFF", value: 0 },
      ],
      autocomplete: true,
      name: "switch",
      type: 4,
      required: true,
      description: "on or off",
    },
  ],
};

ただし、エンドポイント側で

Interaction Type

APPLICATION_COMMAND_AUTOCOMPLETE

を実装する必要があります。
(ユーザーが入力時に、都度表示する選択肢を返してあげる部分の実装)
これについては、記事のボリュームが膨らんでしまいそうなので、別の機会に追加で記事にしたいと思います。

終わりに

今回紹介した内容は、公式ドキュメントの具体例が乏しく、サンプルコードもバリエーションが少なかったり、サンプル内で動的に設定を作成していたりするため、作業のたびに調査に手間取っていた部分でした。
(時間があくと忘れることも多い)

まだまだ試せていないオプションもありますが、作業を通じてだいぶ内容が頭に入ってきたので今後の作業がスムーズになるのではと期待しています。

あえて言えば、資料を作るのに時間がかかりすぎてしまい、費用対効果の面で疑問はありますが、せめて他の技術者の方の助けになれば幸いです。

Discussion

すずねーうすずねーう

DBなどから取得した値を動的に選択させるようなことはできません

autocomplete を使用すればできますよ!

lambtalambta

ありがとうございます!
確認、追記しました。