スラッシュコマンドを使ってみよう
スラッシュコマンドをクライアントに表示するためには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アプリケーションである。
code:register.js
const { 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 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.argv2); console.log("registration succeed!");
}
main().catch(err=>console.error(err));
次のような手順でスラッシュコマンドを登録する。
上記のコードを適当な名前で保存する。
ここでは以下のように保存したものとして進める。
code:cmd
D:.
│ 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現在)anmoti.icon
/と入力欄に打って登録されているか確認してみよう。
以下の画像のように登録されたコマンドが確認できるはずだ。
https://gyazo.com/4b6d0651808debd38c1e04d71bdb3ab0
どちらかのコマンドを実行してみよう。
現時点では失敗するはずだ。
https://gyazo.com/fb0ca1581219c8558818ff742a687985
次は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文字以下である必要がある。
https://gyazo.com/0e383c2b669a7a0d5c4c670c0429db74
value
選択肢の値。
文字列あるいは数字。
typeの型と一致している必要がある。
文字列の場合は100文字以下。
実際に選択肢が選択されたときAPIからはその値が送られてくる。
code:js
const client = new Client({
intents: 0,
});
Clientを宣言している。
Gatewayには接続しないのでintentsは適当に0とした。
code:js
client.token = process.env.DISCORD_TOKEN;
通常loginでトークンを渡すが、loginは呼び出さないのでここでトークンをClientに渡す。
main関数。
code:js
client.application = new ClientApplication(client, {});
await client.application.fetch();
client.applicationは通常readyの発生時に初期化される。
しかし、Gatewayに接続しないためreadyイベントは発生しない。
なのでこちらで初期化する。
しないと、setの呼び出しでエラーになる。
さて、以下がhelloコマンドとpingコマンドの両方を実装したものである。
code:index.js
const 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");
}
};
async function onInteraction(interaction) {
if (!interaction.isCommand()) {
return;
}
}
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から次のような応答が得られるだろう。
https://gyazo.com/8e573062fb7c594f8dceee75906f7a45
/hello language: enと/hello language: jaを実行してみる。
https://gyazo.com/d4e165beed67a87b4e914b19b0e25442https://gyazo.com/57a7b1d5c0c09872f859099960b60b18
解説
on_interaction関数
Interaction型となるような型はいくつかあり、その中でも今回処理したいスラッシュコマンドを表す型はCommandInteractionである。
Interaction#isCommandはInteractionがCommandInteractionであるかを判定するためのメソッドである。
ちなみに、他にはMessageComponentInteractionが定義されており、その配下にButtonInteractionが定義されている。
pingコマンド
code: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によってその呼び出しは失敗したとみなされる。
code:js
await interaction.editReply([...msg, 往復: ${Date.now() - now}ms].join("\n"));
CommandInteraction#editReplyメソッドによってもとの応答を変更することができる。
ここでは最終行にreply関数を呼び出し、その返答が帰ってくるまでの時間を追加している。
helloコマンド
code:js
const lang = interaction.options.get("language");
CommandInteraction#optionsのCollectionのキーはオプション名となっている。
これを用いてlanguageオプションの値を取得した。
スラッシュコマンドが実行されるとinteractionイベントが発生する。
ここではIntentsとして、0(特別なイベントを受信しない)を指定している
満足に動いていることが確認できたならば、node register.jsを実行してグローバルコマンドを登録/更新しよう。
code:registerV14.js
const { 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}!
}
}
}
})