【Discord.js V14】２つのBOTを別々のVCに参加させてみる

皆さん初めまして。Morichanと申します。
下記の※の記述を理解したうえで、記事を読んでください。

※DiscordBot開発初心者の書いた記事です。
（また、プログラミング自体、２年ほどのブランクあり）

BOTをVCに参加させるまでの流れに関しては、[別の記事](https://qiita.com/Mori-chan/items/2df4ad15adc79cf2a167)にまとめてあります。

関連記事：
* [【Windows11】Discord BOTを開発するための開発環境を整える。](https://qiita.com/Mori-chan/items/add6f63b04f70ff53732)
* [【Discord.js V14】Discord BOTにスラッシュコマンドを実装してみる](https://qiita.com/Mori-chan/items/2e869c0e2060b78f0380)
* [【Discord.js V14】Discord BOTをVCに参加させてみる](https://qiita.com/Mori-chan/items/2df4ad15adc79cf2a167)


## 目次
1. [開発環境](#開発環境)
2. [２つ目のBOTを作成する](#２つ目のbotを作成する)
3. [２つのBOTを別々のVCに参加させてみる](#２つのbotを別々のvcに参加させてみる)
4. [関連記事](#関連記事)



## 開発環境
OS (Windows11)
discord.js (14.7.1)
node.js (18.13.0)
npm (8.19.3)
@discordjs/voice (^0.14.0)



## ２つ目のBOTを作成する

* BOTを２つ作成する。
[参考記事](https://qiita.com/Mori-chan/items/add6f63b04f70ff53732)
名前は安直に、Listener-botとSpeaker-botにしました。

APP ICON：
今回は、いらすとやで見つけた、
[よく聞いている人のイラスト（男性）](https://www.irasutoya.com/2014/03/blog-post_2220.html)
[おしゃべりな女性のイラスト](https://www.irasutoya.com/2013/08/blog-post_5392.html)
の２つの画像を使いました。

DESCRIPTION：
Listener-bot ⇒ VC１の音を聞くためのBOTです。
Speaker-bot ⇒ VC１の音をVC２に送信するためのBOTです。

* `config.json`に２つ分のBOTの情報を記述する。

```
{
    "LISTENER": {
        "CLIENT_ID": "ここにListener-botのCLIENT_IDを記述",
        "TOKEN": "ここにListener-botのTOKENを記述"
    },
    "SPEAKER": {
        "CLIENT_ID": "ここにSpeaker-botのCLIENT_IDを記述",
        "TOKEN": "ここにSpeaker-botのTOKENを記述"
    },
    "GUILD_ID": "ここにBOTを追加したサーバーのIDを記述"
}
```

* `index.js`にBOTを２つ動かすための記述を行う。
```
const fs = require('node:fs');
const path = require('node:path');
const { Client, Collection, Events, GatewayIntentBits } = require('discord.js');
const { LISTENER, SPEAKER } = require('./config.json');

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

// 今回はListenner-botに対してのみコマンドを割り当ててみる。
client1.commands = new Collection();

// commandsフォルダから、.jsで終わるファイルのみを取得
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);
	// 取得した.jsファイル内の情報から、コマンドと名前をListenner-botに対して設定
	if ('data' in command && 'execute' in command) {
		client1.commands.set(command.data.name, command);
	} else {
		console.log(`[WARNING]  ${filePath} のコマンドには、必要な "data" または "execute" プロパティがありません。`);
	}
}

// コマンドが送られてきた際の処理
client1.on(Events.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 });
	}
});

client1.once(Events.ClientReady, c => {
    console.log(`Ready! Logged in as ${c.user.tag}`);
});

client2.once(Events.ClientReady, c => {
    console.log(`Ready! Logged in as ${c.user.tag}`);
});

client1.login(LISTENER.TOKEN);
client2.login(SPEAKER.TOKEN);
```


## ２つのBOTを別々のVCに参加させてみる

* VCに入るBOTを識別できるように、BOTの情報を`index.js`から`join.js`に渡す。
(index.js)
```
// 以下の部分を変更
try {
        // コマンドを実行
		await command.execute(interaction, client1, client2);
	} catch (error) {
---（以下略）---
```
(join.js)
```
// 以下の部分を変更
async execute(interaction, client1, client2) {
---（以下略）---
```

* ２つのBOTをVCに参加させるため、コピペして、`join.js`の中身を複数BOT時の挙動に合わせて、記述を改変する。
```
const { SlashCommandBuilder, ChannelType } = require('discord.js');
const { joinVoiceChannel } = require('@discordjs/voice');
module.exports = {
	data: new SlashCommandBuilder()
        // コマンドの名前
		.setName('join')
        // コマンドの説明文
		.setDescription('VCに参加。')
		// コマンドのオプションを追加
		.addChannelOption((option) =>
			option
				.setName('channel1')
				.setDescription('The channel that Listener-bot join')
				.setRequired(true)
				.addChannelTypes(ChannelType.GuildVoice),
		)
		.addChannelOption((option) =>
			option
				.setName('channel2')
				.setDescription('The channel that Speaker-bot join')
				.setRequired(true)
				.addChannelTypes(ChannelType.GuildVoice),
		),
	async execute(interaction, client1, client2) {
		const voiceChannel1 = interaction.options.getChannel('channel1');
		const voiceChannel2 = interaction.options.getChannel('channel2');
        // Listener-botがVCに参加する処理
		const connection1 = joinVoiceChannel({
			// なぜかはわからないが、groupの指定をしないと、先にVCに入っているBOTがVCを移動するだけになってしまうので、記述。
			group: 'listener',
			guildId: interaction.guildId,
			channelId: voiceChannel1.id,
			// どっちのBOTを動かしてあげるかの指定をしてあげる。
			adapterCreator: client1.guilds.cache.get(interaction.guildId).voiceAdapterCreator,
			// VC参加時にマイクミュート、スピーカーミュートにするか否か
			selfMute: true,
			selfDeaf: false,
		});
        // Speaker-botがVCに参加する処理
		const connection2 = joinVoiceChannel({
			group: 'speaker',
			guildId: interaction.guildId,
			channelId: voiceChannel2.id,
			adapterCreator: client2.guilds.cache.get(interaction.guildId).voiceAdapterCreator,
			selfMute: false,
			selfDeaf: true,
		});
		await interaction.reply('参加しました！');
	},
};
```
* 同じVCへ参加できないように、また、voiceChannel1,voiceChannel2に値が入っていないときの処理を記述する。
```
	async execute(interaction, client1, client2) {
		const voiceChannel1 = interaction.options.getChannel('channel1');
		const voiceChannel2 = interaction.options.getChannel('channel2');
		if (voiceChannel1 && voiceChannel2) {
			if (voiceChannel1 === voiceChannel2) {
				await interaction.reply('同じVCには参加できません🥺');
				return;
			}
			// Listener-botがVCに参加する処理
			const connection1 = joinVoiceChannel({

---（以下略）---

				adapterCreator: client2.guilds.cache.get(interaction.guildId).voiceAdapterCreator,
				selfMute: false,
				selfDeaf: true,
			});
			await interaction.reply('VCに参加しました！');
		}
		else {
			await interaction.reply('BOTを参加させるVCを指定してください！');
		}
	},
};
```
* `node deploy-commands.js`、`node index.js`を行い、Discordでコマンドを入力。
無事動きました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3109638/16220cd9-0fd4-b307-edeb-a4b756ce2748.png)

## 関連記事
* [【Windows11】Discord BOTを開発するための開発環境を整える。](https://qiita.com/Mori-chan/items/add6f63b04f70ff53732)
* [【Discord.js V14】Discord BOTにスラッシュコマンドを実装してみる](https://qiita.com/Mori-chan/items/2e869c0e2060b78f0380)
* [【Discord.js V14】Discord BOTをVCに参加させてみる](https://qiita.com/Mori-chan/items/2df4ad15adc79cf2a167)
* 【Discord.js V14】２つのBOTを別々のVCに参加させてみる
* [【Discord.js V14】スラッシュコマンドのオプションで、既に選択済みの選択肢を２つ目の引数のリストから消す。](https://qiita.com/Mori-chan/items/89ad3245443f1e8ace07)
* [【Discord.js V14】Discord BOTにVCに音楽を流す機能を実装してみる](https://qiita.com/Mori-chan/items/84622c733dc43c419dc5)
* [【Discord.js V14】Discord BOTにVCから１人分の音声を取得して、録音する機能を実装してみる](https://qiita.com/Mori-chan/items/8fcb1770c4d0020a8284)
* [【Discord.js V14】VC１からVC２に音声をStreaming（中継）するBOTを作成する。](https://qiita.com/Mori-chan/items/e9ced87c0d68a9eb2a9a)
* [【Discord.js V14】取得するVCの音声をロールによって判別する](https://qiita.com/Mori-chan/items/41cd914bb9a93d930bff)