discord.js でスラッシュコマンド（Slash commands）を使う

# はじめに

今まで私は Python で discord.py というライブラリを使って Discord Bot を開発していたのですが discord.py の開発が終了してしまったためもう一つの有名なライブラリである discord.js を使ってみようと思い、今まで触ってこなかった Node.js を勉強しはじめました。そして discord.js はスラッシュコマンドに対応しているということで、これで色々遊んでみようと思います。

追記（2022/5/4）：
想像以上の反響に私も驚いています．私は JavaScript 初心者な上に今は [Pycord](https://github.com/Pycord-Development/pycord) を使って Bot 開発をしているので Discord.js の方は追えていません．予めご了承ください．

## 前提

- 基本的な JavaScript の文法を知っている
- discord.js を少しでも触ったことがある
- Node.js の環境がある
- Discord の Bot のトークンを持っている

## 著者の環境

- macOS Big Sur 11.5.2
- Node.js v16.10.0
- discord.js v13.1.0

**スラッシュコマンドを使うには　discord.js v13 以上が必要です。そしてそれには　Node.js v16 以上が必要です。**

# 本題

## スラッシュコマンドの登録

スラッシュコマンドにはギルドコマンドとグローバルコマンドの二種類があります。ギルドコマンドは指定したサーバーでしか使うことができませんがグローバルコマンドは登録にちょっと時間がかかるので今回はギルドコマンドを使います。

ギルドコマンドを登録するためには Bot をサーバーに追加する際、application.commands にチェックを入れる必要があります。すでにサーバーに入っている場合は脱退させずにもう一度この URL を踏めばいいらしいです。

![スクリーンショット 2021-09-26 15.32.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/756d0428-c9a5-5c82-7238-18b18ee1acbe.png)

Bot の起動時に登録したいため以下のようにします。

```
./index.js
./.env
```

というファイルを作ってそれぞれ

```js:./index.js
const { Client, Intents } = require('discord.js');
const dotenv = require('dotenv');

dotenv.config();

const client = new Client({ intents: [Intents.FLAGS.GUILDS] });

client.once("ready", async () => {
    const data = [{
        name: "ping",
        description: "Replies with Pong!",
    }];
    await client.application.commands.set(data, 'server_id');
    console.log("Ready!");
});

client.login(process.env.DISCORD_TOKEN);
```

```:./.env
DISCORD_TOKEN="Your_Discord_Bot_Token"
```

にします。

`server_id` はスラッシュコマンドを追加したいサーバーの ID、`Your_Discord_Bot_Token` は Bot のトークンに書き換えてください。 `'server_id'` という引数をなくせばグローバルコマンドになります。実行するたびにすべてのコマンドは上書きされるので気楽に登録して大丈夫です。

これで実行すると

![スクリーンショット 2021-09-26 15.24.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/682e8336-4b2f-08ea-97a4-61779d89d6cd.png)

このように今追加したコマンドが出てきますが

![スクリーンショット 2021-09-26 15.25.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/5e05c5f4-e060-d5ab-7ad2-5f4cc14ff1aa.png)

まだ反応する処理を書いていないのでこのようになります。ではそれを書いていきましょう。（ちなみに背景が白いのは Discord のテーマをライトにしているだけです。）

## スラッシュコマンドに反応する処理を書く

説明するよりコードを見たほうが早いと思うのでコードを貼ります。

```js:./index.js
client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    if (interaction.commandName === 'ping') {
        await interaction.reply('Pong！');
    }
});
```

コードを読めば多分なんとなく分かると思うのですが、この interaction を使って色々できます。`console.log(interaction)` とかすれば色々見れます。

これをさっきのコードに追加して実行すれば

![スクリーンショット 2021-09-26 15.59.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/9316d998-5473-bea1-613d-253353ffeb73.png)

できました！

### エフェメラルレスポンス

Ephemeral responses　の日本語訳はエフェメラルレスポンスでいいのか不安になりながら書いていきます。これは実行した人にしか見えないようにするというものです。

```js:./index.js
client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    if (interaction.commandName === 'ping') {
        await interaction.reply({ content: 'Pong!', ephemeral: true });
    }
});
```

![スクリーンショット 2021-09-26 18.16.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/8fda2119-5173-10b5-5f1f-a0f30136eacf.png)

どうでもいいですが夜になったら Discord のテーマがダークになるようにしています。

### レスポンスの編集

最初のレスポンスから１５分間編集できます。

```js:./index.js
const wait = require('util').promisify(setTimeout);

client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    if (interaction.commandName === 'ping') {
        await interaction.reply('Pong!');
        await wait(2000);
        await interaction.editReply('Pong again!');
    }
});
```

２秒後に編集されます。

![スクリーンショット 2021-09-26 18.24.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/b4f7fe91-3844-be87-3499-ea83aa187e6d.png)

### 遅れた反応

通常３秒間反応がないとインタラクションに失敗しましたとなってしまうのですが `deferReply` を使うと３秒以上遅れて反応することができます。

```js:./index.js
const wait = require('util').promisify(setTimeout);

client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    if (interaction.commandName === 'ping') {
        await interaction.deferReply();
        await wait(4000);
        await interaction.editReply('Pong!');
    }
});
```

![スクリーンショット 2021-09-26 18.35.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/f8d25cd0-4895-08a7-d5f7-1fc5664d4aca.png)

### フォローアップ

続けて反応する事ができます。これも最初の反応から１５分間できます。語彙力がないのでコードと画像を見てください。

```js:./index.js
client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    if (interaction.commandName === 'ping') {
        await interaction.reply('Pong!');
        await interaction.followUp('Pong again!');
    }
});
```

![スクリーンショット 2021-09-26 18.40.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/94cd4c35-822a-ca7a-69a2-239a3be09698.png)

### レスポンスの削除

レスポンスを消すこともできます。エフェメラルレスポンスは削除できません。

```js:./index.js
const wait = require('util').promisify(setTimeout);

client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    if (interaction.commandName === 'ping') {
        await interaction.reply('Pong!');
        await wait(2000);
        await interaction.deleteReply();
    }

});
```

上のコードではレスポンスしてから２秒後に削除しています。

### レスポンスの Message オブジェクトの取得

レスポンスの Message オブジェクトを取得したい場合は `fetchReply` を使います。

```js:./index.js
client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    if (interaction.commandName === 'ping') {
        await interaction.reply('Pong!');
        const message = await interaction.fetchReply();
        console.log(message);
    }
});
```

## 簡単なフレームワークを作る

このままコマンドを追加していってもいいのですがコマンドが増えるとごちゃごちゃしてしまうのでコマンドひとつひとつ別のファイルに書けるようにします。

```
./index.js
./.env
./commands/ping.js
```

として、

```js:index.js
const fs = require('fs')
const { Client, Intents } = require('discord.js');
const dotenv = require('dotenv');

dotenv.config();

const client = new Client({ intents: [Intents.FLAGS.GUILDS] });

const commands = {}
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'))

for (const file of commandFiles) {
    const command = require(`./commands/${file}`);
    commands[command.data.name] = command
}

client.once("ready", async () => {
    const data = []
    for (const commandName in commands) {
        data.push(commands[commandName].data)
    }
    await client.application.commands.set(data, 'server_id');
    console.log("Ready!");
});

client.on("interactionCreate", async (interaction) => {
    if (!interaction.isCommand()) {
        return;
    }
    const command = commands[interaction.commandName];
    try {
        await command.execute(interaction);
    } catch (error) {
        console.error(error);
        await interaction.reply({
            content: 'There was an error while executing this command!',
            ephemeral: true,
        })
    }
});

client.login(process.env.DISCORD_TOKEN);
```

.env は変更なし。

```js:./commands/ping.js
module.exports = {
	data: {
        name: "ping",
        description: "Replies with Pong!",
    },
	async execute(interaction) {
		await interaction.reply('Pong!');
	}
}
```

JavaScript 初心者とはいえ我ながらクソコードを書いたと思います。もっと良い実装があったらぜひ編集リクエストしてください。これで　`./commands` 以下に `ping.js` のようなファイルを作ることでコマンドを追加することができます。

## 様々なオプション

新しいファイルを作ります。

```js:./commands/echo.js
module.exports = {
    data: {
        name: "echo",
        description: "Replies with your input!",
        options: [{
            type: "STRING",
            name: "input",
            description: "The input to echo back",
            required: true
        }],
    },
    async execute(interaction) {
        await interaction.reply(interaction.options.getString('input'))
	}
}
```

こんな感じでオプションを追加することができます。`options` の value は配列なので中身を複数にすれば複数のオプションを作ることができます。

`type` の value は
- `SUB_COMMAND`
- `SUB_COMMAND_GROUP`
- `STRING`
- `INTEGER`
- `BOOLEAN`
- `USER`
- `CHANNEL`
- `ROLE`
- `MENTIONABLE`
- `NUMBER`
から選べます。

### 選択肢を作る

```js:./commands/greet.js
module.exports = {
    data: {
        name: "greet",
        description: "Greets!",
        options: [{
            type: "STRING",
            name: "language",
            description: "Choice your language",
            required: true,
            choices: [
                { name: "Japanese", value: "japanese" },
                { name: "English", value: "english" },
            ]
        }],
    },
    async execute(interaction) {
        if (interaction.options.getString('language') === 'japanese') {
			await interaction.reply('こんにちは！');
        } else {
            await interaction.reply('Hello!');
        }
    }
}
```

こんな感じで選べて

![スクリーンショット 2021-09-30 16.08.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/09244bac-e97b-9908-ee46-eaf53594e337.png)

こんな感じで返事できます。

![スクリーンショット 2021-09-30 16.09.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/f77d13a6-03e7-3b76-3977-462d1ec95ed4.png)

### サブコマンド

```js:./commands/info.js
module.exports = {
    data: {
        name: "info",
        description: "Get info about a user or a server!",
        options: [
            {
                type: "SUB_COMMAND",
                name: "user",
                description: "Info about a user",
                options: [{
                    type: "USER",
                    name: "target",
                    description: "The user",
                }],
            },
            {
                type: "SUB_COMMAND",
                name: "server",
                description: "Info about the server",
            },
        ],
    },
    async execute(interaction) {
        if (interaction.commandName === 'info') {
            if (interaction.options.getSubcommand() === 'user') {
                const user = interaction.options.getUser('target');

                if (user) {
                    await interaction.reply(`Username: ${user.username}\nID: ${user.id}`);
                } else {
                    await interaction.reply(`Your username: ${interaction.user.username}\nYour ID: ${interaction.user.id}`);
                }
            } else if (interaction.options.getSubcommand() === 'server') {
                await interaction.reply(`Server name: ${interaction.guild.name}\nTotal members: ${interaction.guild.memberCount}`);
            }
        }
    }
}
```

![スクリーンショット 2021-09-30 16.25.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625302/1becafbd-17c3-5ea8-ba0a-dbbced8c1355.png)

# 参考にさせていただいたサイト

[discord.js 公式ドキュメント](https://discord.js.org/#/docs/main/13.1.0/typedef/ApplicationCommandData)
[Discord.js Guide](https://discordjs.guide/)
[【discord.js】v13でbotを作ってみる](https://qiita.com/yuu_1st/items/aaa6ea92408726a0157e)
[Node.jsでDiscordBotを作るのに苦戦した話](https://qiita.com/MWindows99/items/3a04349a30bdda400674)

## この記事のライセンス
[![クリエイティブ・コモンズ・ライセンス](https://i.creativecommons.org/l/by/4.0/88x31.png)](http://creativecommons.org/licenses/by/4.0/deed.ja)
この記事は[CC BY 4.0（クリエイティブ・コモンズ 表示 4.0 国際 ライセンス）](http://creativecommons.org/licenses/by/4.0/deed.ja)の元で公開します。