【3日目】discord.jsでスラッシュコマンドを作ってみよう

# はじめに
こんにちは、なりかくんと申します。
今回は、discord.jsを使ってスラッシュコマンドの登録からコマンド処理の作成までやってみようと思います。

# スラッシュコマンドの注意
まずスラッシュコマンドは、サーバーに`applications.commands`のスコープを承認される必要があります。サーバーにBotを追加するときに、Discord Developer PortalのOAuth2タブからURLの作成を行うと思います。
そこで、SCOPEを選択する画面を見覚えがありませんか？そこに`applications.commands`スコープがあるのですが、これを有効にしないと追加したアプリケーションでスラッシュコマンドが表示されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/0651e918-3e0d-025e-6d57-9e7e13ef7b9f.png)

なお、もし既にサーバーに追加済みの場合でも`applications.commands`だけのリンクを作って再度認証しなおせば再導入無しで承認することが可能です。（`bot`スコープがあっても問題ありません。）

# ギルドコマンドとグローバルコマンド
まず、Discord Botにはギルドコマンドとグローバルコマンドの2種類があります。
ギルドコマンドは、名前の通りサーバーごとに設定できるコマンドですぐに反映されるので開発にはギルドコマンドを利用することが一般的です。
そして、グローバルコマンドは想像通り、導入されているサーバー（`applications.commands`の有効が必要）全体に反映されるコマンドです。反映までに数時間かかり１日あたりの作成・更新に制限があります。

また、開発用と公開用でBotを分けておくとギルドコマンドとグローバルコマンドのコマンドの重複を防ぐことが出来ます。

# コマンドファイル構造
今回は、コマンドごとにファイルを分けて作っていきます。ファイル構造としては以下のようになります。`commands`フォルダーを新しく作りました。この中にコマンドを追加していきます。
```
discord-bot/
├── node_modules
├── commands
├── config.json
├── index.js
├── package-lock.json
└── package.json
```

# コンフィグファイル
2日目は、コードに直接Tokenを書きましたが`config.json`を作って管理をしやすくします。
後ほど使うので`clientId`と`guildId`もついでに追加しておきましょう。`clientId`は、BotのIdです。`guildId`は、開発に使うサーバーIdです。
```json
{
	"token": "your-token",
	"clientId": "your-clientId",
	"guildId": "your-guildId"
}
```

# コマンドファイル
今回は、Pingコマンドを作ります。これは、コマンドが実行されたら`Pong!`と返すコマンドです。
`commands`の中に`ping.js`というファイルを作ります。
```js:commands/ping.js
const { SlashCommandBuilder } = require('discord.js');

module.exports = {
	data: new SlashCommandBuilder()
		.setName('ping')
		.setDescription('Pongを返します'),
	async execute(interaction) {
		await interaction.reply('Pong!');
	},
};
```
`SlashCommandBuilder`というスラッシュコマンドを作る際には非常に便利なものが用意されていますのでそれを利用します。`.setName()`の内容がコマンド名になります。

# ギルドコマンドの登録
では、今回はギルドコマンドの登録を行っていきます。 `deploy-commands.js`というファイルを作ります。これは、Discord APIにコマンド登録を行う用のプログラムです。
```js:deply-commands.js
const { REST, Routes } = require('discord.js');
const { clientId, guildId, token } = require('./config.json');
const fs = require('node:fs');

const commands = [];
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
	const command = require(`./commands/${file}`);
	commands.push(command.data.toJSON());
}

const rest = new REST({ version: '10' }).setToken(token);

(async () => {
	try {
		console.log(`${commands.length} 個のアプリケーションコマンドを登録します。`);

		const data = await rest.put(
			Routes.applicationGuildCommands(clientId, guildId),
			{ body: commands },
		);

		console.log(`${data.length} 個のアプリケーションコマンドを登録しました。`);
	} catch (error) {
		console.error(error);
	}
})();
```

`commands`の中にあるデータをDiscord APIに投げているだけの簡単なプログラムです。

これを実行すると現在はコマンドファイルが１つなので以下のようになります。
```shell
> node deploy-commands.js
1 個のアプリケーションコマンドを登録します。
1 個のアプリケーションコマンドを登録しました。
```

確認してみると、しっかり登録が出来ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/2a2d3eeb-5791-8280-530b-29e3687dbd32.png)

# グローバルコマンドにする
先ほどはギルドコマンドで登録しましたが、これをグローバルコマンドにしたい場合は`applicationGuildCommands`を少し変えるとグローバルコマンドに変更することが出来ます。
```js
await rest.put(
	Routes.applicationCommands(clientId),
	{ body: commands },
);
```

# コマンド処理をする
では、登録したコマンドを処理するプログラムを書きます。`index.js`のコードです。
```js:index.js
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, GatewayIntentBits } = require('discord.js');
const { token } = require('./config.json');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.on('ready', () => {
	console.log(`${client.user.tag}でログインしました。`);
});

client.login(token);

client.commands = new Collection();

const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
	const filePath = path.join(commandsPath, file);
	const command = require(filePath);
	if ('data' in command && 'execute' in command) {
		client.commands.set(command.data.name, command);
	} else {
		console.log(`${filePath} に必要な "data" か "execute" がありません。`);
	}
}

client.on('interactionCreate', async interaction => {
	if (!interaction.isChatInputCommand()) return;

	const command = interaction.client.commands.get(interaction.commandName);

	if (!command) {
		console.error(`${interaction.commandName} が見つかりません。`);
		return;
	}

	try {
		await command.execute(interaction);
	} catch (error) {
		console.error(error);
		await interaction.reply({ content: 'エラーが発生しました。', ephemeral: true });
	}
});
```
簡単にコードの解説です。
これは、コマンドファイルを読み込んで、Collectionに登録しているプログラムです。
```js
client.commands = new Collection();

const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));

for (const file of commandFiles) {
	const filePath = path.join(commandsPath, file);
	const command = require(filePath);
	if ('data' in command && 'execute' in command) {
		client.commands.set(command.data.name, command);
	} else {
		console.log(`${filePath} に必要な "data" か "execute" がありません。`);
	}
}
```
そして、`interactionCreate`でイベントを受け取ります。
`.isChatInputCommand()`は、`interactionCreate`ではスラッシュコマンド以外のイベントも受け取るためスラッシュコマンドかどうかを判断するコードです。
```js
if (!interaction.isChatInputCommand()) return;
```

実際に実行してみると、`Pong!`としっかり返ってくることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549951/e3fe0fe2-703b-2a5f-05ed-4056083d350e.png)

以上ですっ、最後までお読みいただきありがとうございました。