はじめに
Node.js よりも高速で TypeScript を標準サポートする Bun を使い、実戦的な構造の Discord Bot を作成します。
今回は「ただ動かすだけ」ではなく、コマンドやイベントをファイルごとに分割して管理する、拡張性の高いベースコードを構築します。
動作環境
- Bun: v1.0.0+
- discord.js: v14.x
1. 事前準備
前回の記事を参考にしてください。
これから、前回の記事にプラスで必要な設定を行っていきます。
環境変数の設定
.env ファイルを作成し、各種情報を入力します。
GUILD_ID は開発中にコマンドを即時反映させるために使用します。
.env
TOKEN=あなたのBotトークン
CLIENT_ID=あなたのBotのアプリケーションID
GUILD_ID=開発用サーバーのID
2. フォルダ構成
今回作成する構成は以下の通りです。
.
├── index.ts # エントリーポイント
├── events/ # イベントハンドラー
│ ├── messageCreate.ts # メッセージ受信イベント
│ └── slashCreate.ts # スラッシュコマンド実行イベント
├── commands/ # スラッシュコマンド定義
│ └── ping.ts # サンプルコマンド
└── .env
3. コードの実装
① index.ts(メイン処理)
このファイルが Bot の心臓部です。コマンドやイベントファイルを自動でスキャンして読み込み、Slash Command の登録まで行います。
index.ts
import { Client, Collection, GatewayIntentBits, Partials, REST, Routes } from "discord.js";
import { Glob } from "bun";
import path from "node:path";
// Client型を拡張して、コマンドを保持するCollectionを追加
export interface ExtendedClient extends Client {
slashCommands: Collection<string, any>;
[key: string]: any; // 他のコレクション用
}
// クライアントのインスタンスを作成
const client = new Client({
intents: [
GatewayIntentBits.Guilds, // ギルド(サーバー)に関するイベント
GatewayIntentBits.GuildMessages, // ギルド内メッセージに関するイベント
GatewayIntentBits.MessageContent, // メッセージの内容を読み取るためのインテント
],
}) as ExtendedClient;
// 各種コマンド保存用のCollectionを初期化
const collections = ["slashCommands"];
collections.forEach((c) => (client[c] = new Collection()));
// --- ハンドラー:イベント読み込み ---
const eventFiles = new Glob("events/*.ts").scanSync(".");
for (const file of eventFiles) {
const event = (await import(path.join(import.meta.dir, file))).default;
if (event.once) {
client.once(event.name, (...args) => event.execute(...args, client));
} else {
client.on(event.name, (...args) => event.execute(...args, client));
}
}
// --- ハンドラー:スラッシュコマンド読み込み ---
const slashFiles = new Glob("commands/*.ts").scanSync(".");
for (const file of slashFiles) {
const command = (await import(path.join(import.meta.dir, file))).default;
if (command?.data?.name) {
client.slashCommands.set(command.data.name, command);
}
}
const rest = new REST({ version: "10" }).setToken(process.env.TOKEN!);
async () => {
try {
console.log("コマンドの読み込みを開始します...");
const commandJsonData = Array.from(client.slashCommands.values()).map((c) => c.data.toJSON());
const clientId = process.env.CLIENT_ID!;
const guildId = process.env.GUILD_ID!;
await rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commandJsonData });
console.log("コマンドの読み込みが完了しました。");
await client.login(process.env.TOKEN);
console.log(`ログインしました。${client.user?.tag}`);
} catch (error) {
console.error("起動に失敗しました。", error);
}
};
② events/messageCreate.ts(メッセージ反応)
従来の「!」から始まるコマンドや、特定のワードへの反応を記述します。
events/messageCreate.ts
import { Events, Message } from 'discord.js'
export default {
name: Events.MessageCreate,
execute(message: Message) {
// Bot自身のメッセージには反応しない
if (message.author.bot) return
if (message.content === '!ping') {
message.reply('Pong! (Legacy Command)')
}
},
}
③ events/slashCreate.ts(スラッシュコマンド処理)
/(スラッシュ)コマンドが打たれた際、index.tsで読み込んだコマンド集から該当する処理を呼び出します。
events/slashCreate.ts
import { Events, Interaction } from 'discord.js'
import type { ExtendedClient } from '../index'
export default {
name: Events.InteractionCreate,
async execute(interaction: Interaction, client: ExtendedClient) {
if (!interaction.isChatInputCommand()) return
const command = client.slashCommands.get(interaction.commandName)
if (!command) return
try {
await command.execute(interaction)
} catch (error) {
console.error(error)
await interaction.reply({ content: 'コマンド実行中にエラーが発生しました。', ephemeral: true })
}
},
}
④ commands/ping.ts(サンプルコマンド)
テスト用の簡単なスラッシュコマンドです。
commands/ping.ts
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'
export default {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Pong!と返します'),
async execute(interaction: ChatInputCommandInteraction) {
await interaction.reply('🏓 Pong! by Bun')
},
}
4. 実行
以下のコマンドで Bot を起動します。
bun index.ts
- Discord上で
!pingと打つと反応します。 -
/pingコマンドが登録され、使用可能になります(開発用サーバーですぐに確認できます)。
5. ポイントの解説
-
Bun の
Globスキャン:
new Glob(...).scanSync('.')を使うことで、ファイルが増えてもindex.tsを書き換えることなく、自動で新しいコマンドやイベントが読み込まれます。 -
REST API によるコマンド登録:
開発時はGUILD_IDを指定することで、Discord側のコマンド更新を即時反映させています
おわりに
この構成をベースにすることで、commands/ にファイルを追加するだけでどんどん機能を追加していくことができます。