スラッシュコマンドを使ってみよう
スラッシュコマンドをクライアントに表示するためにはDiscordへコマンドを登録する必要がある。
また、スラッシュコマンドを表示/使用するためにはアプリケーションに対してサーバーへの導入時に applications.commands
スコープが与えられていなければならない。
bot
スコープは
Interactionsに付属してくるトークンのみで呼び出せないAPIなどを呼び出す場合、Gatewayイベントを使用する場合などに依然として必要である。
導入URLの例をいくらか示す。
2021/3/26以前にボットがサーバーに導入されており、以後Kick、Banされていない場合、Botは applications.commands
スコープを持っていると考えて差し支えない。
言い換えると Guild#joinedAt
が2021/3/26以前ならば、スラッシュコマンドを登録さえすればサーバーで使用できるということである。
> Scope migration: Applications with the bot scope were granted the applications.commands scope. Starting today, you should add applications.commands to your invite link if you plan to use slash commands.
そうでない場合は利用者に applications.commands
スコープを付与して貰う必要がある
以下がスラッシュコマンドの登録に使う簡単なCLIアプリケーションである。
register.jsconst { Client, ClientApplication } = require("discord.js");
/**
*
* @param {Client} client
* @param {import("discord.js").ApplicationCommandData[]} commands
* @param {import("discord.js").Snowflake} guildID
* @returns {Promise<import("@discordjs/collection").Collection<string,import("discord.js").ApplicationCommand>>}
*/
async function register(client, commands, guildID) {
if (guildID == null) {
return client.application.commands.set(commands);
}
return client.application.commands.set(commands, guildID);
}
const ping = {
name: "ping",
description: "pong!",
};
const hello = {
name: "hello",
description: "botがあなたに挨拶します。",
options: [
{
type: "STRING",
name: "language",
description: "どの言語で挨拶するか指定します。",
required: true,
choices: [
{
name: "English",
value: "en"
},
{
name: "Japanese",
value: "ja"
}
],
}
]
};
const commands = [ping, hello];
const client = new Client({
intents: 0,
});
client.token = process.env.DISCORD_TOKEN;
async function main() {
client.application = new ClientApplication(client, {});
await client.application.fetch();
await register(client, commands, process.argv[2]);
console.log("registration succeed!");
}
main().catch(err=>console.error(err));
次のような手順でスラッシュコマンドを登録する。
上記のコードを適当な名前で保存する。
ここでは以下のように保存したものとして進める。
cmdD:.
│ package.json
│ register.js
│ yarn.lock
node register.js <your guild id>
を実行する。
うまく行けば registration succeed!
とコンソールに出るだろう。
トラブルシューティング
Error: Cannot find module 'D:\djs-v13\regist.js'
jsファイルが見つからない。
D:\djs-v13\regist.js
にファイルは存在するか?
カレントディレクトリは正しい?
ファイル名を間違えてない?
HTTPError [DiscordjsError]: Request to use token, but token was unavailable to the client.
Discord.jsのクライアントにトークンがわたされていない。
環境変数にトークンを設定した?
環境変数の名前を間違えていない?
DiscordAPIError: 401: Unauthorized
のうち path: "/oauth2/applications/@me"
であるもの
誤ったトークンを入力していない?
DiscordAPIError: Missing Access
のうち code:50001
であり path:'/guilds/<your guild id>?with_counts=true'
であるもの
サーバーにbotは導入されている?
DiscordAPIError: Missing Access
のうち code:50001
であり path: '/applications/<your application id>/guilds/<your guild id>/commands
であるもの
applications.commands
つきでアプリケーションを導入した?
その後Discordクライアントを再起動する。
PCのクライアントを使用している場合はCtrl+Rでもよい。
再起動しなくてもロードされます(WindowsStable, 2021.12.28現在)

/と入力欄に打って登録されているか確認してみよう。
以下の画像のように登録されたコマンドが確認できるはずだ。
どちらかのコマンドを実行してみよう。
現時点では失敗するはずだ。
次はbotが応答を返せるようにしていこう。
解説
register
関数
guildID
が null
または undefined
である場合グローバルコマンドを登録し、関数はその返り値を返却する。
グローバルコマンドの登録は client.application.commands.set
というメソッドで行っている。
client.application
はnullableであるが、ここではそのようなことは起き得ないものとして進める。(mainの実装をみれば明らかになるだろうし、 login
(後にコマンドに実際に応答させるときに触れる)後は特別に利用者が代入しなければ、nullになりえない)
guildID
としてサーバーidがわたされてきた場合、そのサーバーidを持つGuildを取得し、そのサーバーへコマンドを登録する。
このようにして guildID
がわたされた場合はサーバー単位でコマンドを登録し、そうではなくnullishな値が渡された(あるいは何もわたされなかった)場合はグローバルコマンドを登録する関数が定義できた。
ping
変数を見ていこう。
これは /ping
スラッシュコマンドの定義である。
つまり、 ApplicationCommandData
である。
name
コマンドの名前を指定する。
これは必須である。
discordはコマンド名に以下のような制約を課している。
アプリケーションは同じ名前を持つ2つのグローバルコマンドを持つことはできない。
アプリケーションは同じ名前を持つ2つのギルドコマンドを持つことはできない。
同じ名前のグローバルコマンドとギルドコマンドを持つことは可能。
複数のアプリケーションが同じ名前を持つことは可能。
正規表現 ^[\w-]{1,32}$
に一致し、かつlower caseである必要がある。
lower caseというのは小文字という意味である。
つまり、 a
から z
までの英小文字と、 0
から 9
までの数字、それに加えて -
と _
で構成された1文字以上32文字以下の文字列であることが求められている。
description
コマンドの説明を指定する。
これも必須である。
1から100文字である必要がある。
hello
変数も見てみよう。
これは /hello
コマンドの定義である。
名前は hello
で、コマンドの説明は botがあなたに挨拶します。
である。
options
これはコマンドのオプション(関数の引数とでも思ったほうがわかりやすいかもしれない)を定義する。
順番が保持される。
定義順にUIに現れる。
コマンドは全部で25個のオプションを持つことができる。
ここでは1つだけ引数を定義していてその名前は language
である。
[0]
type
引数の型を指定する。
必須。
今回は文字列とした。
name
オプションの名前
ここでは language
コマンド名と同様の制約が課される。
description
オプションの説明。
1から100文字である必要がある。
required
このオプションが必須であるか。
ここでは、オプションは必須とした。
required
自体を任意で省略した場合は false
、このオプションを指定するかは任意になる。
choices
[idx]
name
選択肢の名前
1文字以上100文字以下である必要がある。
value
選択肢の値。
文字列あるいは数字。
type
の型と一致している必要がある。
文字列の場合は100文字以下。
実際に選択肢が選択されたときAPIからはその値が送られてくる。
js const client = new Client({
intents: 0,
});
Clientを宣言している。
Gatewayには接続しないので intents
は適当に0とした。
jsclient.token = process.env.DISCORD_TOKEN;
通常 login
でトークンを渡すが、 login
は呼び出さないのでここでトークンをClientに渡す。
main
関数。
js client.application = new ClientApplication(client, {});
await client.application.fetch();
client.application
は通常 ready
の発生時に初期化される。
しかし、Gatewayに接続しないため ready
イベントは発生しない。
なのでこちらで初期化する。
しないと、 set
の呼び出しでエラーになる。
さて、以下がhelloコマンドとpingコマンドの両方を実装したものである。
index.jsconst Discord = require("discord.js");
const commands = {
/**
*
* @param {Discord.CommandInteraction} interaction
* @returns
*/
async ping(interaction) {
const now = Date.now();
const msg = [
"pong!",
"",
`gateway: ${interaction.client.ws.ping}ms`,
];
await interaction.reply({ content: msg.join("\n"), ephemeral: true });
await interaction.editReply([...msg, `往復: ${Date.now() - now}ms`].join("\n"));
return;
},
/**
*
* @param {Discord.CommandInteraction} interaction
* @returns
*/
async hello(interaction) {
const source = {
en(name){
return `Hello, ${name}!`
},
ja(name){
return `こんにちは、${name}さん。`
}
};
const name = interaction.member?.displayName ?? interaction.user.username;
const lang = interaction.options.get("language");
return interaction.reply(source[lang.value](name));
}
};
async function onInteraction(interaction) {
if (!interaction.isCommand()) {
return;
}
return commands[interaction.commandName](interaction);
}
const client = new Discord.Client({
intents: 0
});
client.on("interactionCreate", interaction => onInteraction(interaction).catch(err => console.error(err)));
client.login(process.env.DISCORD_TOKEN).catch(err => {
console.error(err);
process.exit(-1);
});
以下のような手順で実行する。
node index.js
を実行する。
入力欄より /ping
コマンドを実行する
うまく行けばbotから次のような応答が得られるだろう。
/hello language: en
と /hello language: ja
を実行してみる。
解説
on_interaction
関数
Interaction
型となるような型はいくつかあり、その中でも今回処理したいスラッシュコマンドを表す型は CommandInteraction
である。
Interaction#isCommand
は Interaction
が CommandInteraction
であるかを判定するためのメソッドである。
ちなみに、他には MessageComponentInteraction
が定義されており、その配下に ButtonInteraction
が定義されている。
ping
コマンド
js const msg = [
"pong!",
"",
`gateway: ${interaction.client.ws.ping}ms`,
];
await interaction.reply({ content: msg.join("\n"), ephemeral: true });
ephemeral: true
は応答を ephemeral
なものとするというオプションを指定しているものである。
ephemeralなメッセージは次のような特徴を持つ
送信されてきた人にしか見えない。
スラッシュコマンドへの応答としてのみ送信可能。
閉じたり(Dissmiss messageを押す)、時間が経ったり、Discordを再起動したりした場合には消える。
defer
あるいは reply
を三秒以内に呼び出さない場合discordによってその呼び出しは失敗したとみなされる。
js await interaction.editReply([...msg, `往復: ${Date.now() - now}ms`].join("\n"));
CommandInteraction#editReply
メソッドによってもとの応答を変更することができる。
ここでは最終行に reply
関数を呼び出し、その返答が帰ってくるまでの時間を追加している。
hello
コマンド
js const lang = interaction.options.get("language");
CommandInteraction#options
のCollectionのキーはオプション名となっている。
これを用いて language
オプションの値を取得した。
スラッシュコマンドが実行されると interaction
イベントが発生する。
ここではIntentsとして、 0
(特別なイベントを受信しない)を指定している
満足に動いていることが確認できたならば、 node register.js
を実行してグローバルコマンドを登録/更新しよう。
registerV14.jsconst { Client, GatewayIntentBits } = require("discord.js")
const client = new Client({
intents: [
GatewayIntentBits.Guilds
]
})
client.on("interactionCreate", async (interaction) => {
if (interaction.isChatInputCommand()){
const { commandName } = interaction
if (commandName === "ping"){
const now = Date.now()
const text = `pong!\n\ngateway:${client.ws.ping}ms`
await interaction.reply({
content: text,
ephemeral: true
})
return interaction.editReply(`${text}\n往復:${Date.now() - now}ms`)
} else if (commandName === "hello"){
const lang = {
ja: (name) => `こんにちは、${name}さん。`,
en: (name) => `Hello, ${name}!`
}
return interaction.reply(lang[interaction.options.getString("language")](interaction.member?.displayName || interaction.user.username))
}
}
})