Open8
Discordボットの基礎導入(Nodejs/TS)
Discord.jsで自鯖用Botを開発した記録
このドキュメントは、僕がDiscordサーバー用にBotを開発した際の手順と内容をまとめたものです。以下の手順に沿って開発をはじめました。
手順
1. npmで初期化
最初にプロジェクトのディレクトリを作成し、npm init
コマンドでプロジェクトを初期化します。これは、package.json
を作成し、プロジェクトの管理を簡単にするための手順です。
npm init -y
この後必要情報は自分で書き換えてもらえればok
例でpackage.jsonとtsconfig.jsonを提示しておく
- package.json
{
"name": botbotbot",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"dev": "npm run compile && node build/deploy-commands.js && node build/main.js",
"start": "node build/main.js",
"compile": "tsc -p ."
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@discordjs/voice": "^0.17.0",
"@supabase/supabase-js": "^2.45.4",
"discord.js": "^14.16.3",
"dotenv": "^16.4.5",
"uuid": "^10.0.0",
"ytdl-core": "^4.11.5"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/node": "^22.7.4",
"eslint": "^9.11.1",
"globals": "^15.10.0",
"supabase": "^1.200.3",
"ts-node": "^10.9.2",
"typescript": "^5.6.2",
"typescript-eslint": "^8.8.0"
}
}
- tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./build",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
2. 必要なパッケージのインストール
プロジェクトで使用する主要なパッケージと、開発時に役立つdevDependenciesをインストールします。
通常パッケージ:
- @supabase/supabase-js: Supabaseと連携するためのライブラリ
- discord.js: Discord APIとやり取りするためのメインライブラリ
- dotenv: 環境変数を扱うためのライブラリ
- uuid: 一意のIDを生成するためのライブラリ
開発用パッケージ(devDependencies):
- @eslint/js: JavaScriptのコード品質を保つためのESLint
- @types/node: Node.jsの型定義
- eslint: コード品質チェック用ツール
- globals: ESLint用のグローバル変数定義
- supabase: Supabaseのクライアント
- typescript: TypeScriptサポート
- typescript-eslint: TypeScriptとESLintの連携
npm install @supabase/supabase-js discord.js dotenv uuid
npm install --save-dev @eslint/js @types/node eslint globals supabase typescript typescript-eslint
3. main.tsとdeploy-commands.tsの作成 + env
DiscordのAPIとやり取りするためのファイルを作成します。
ついでにenvファイルの仮の状態も提供します。
- .env
# NODE_ENV=development
NODE_ENV=production
# Discordの開発portalから取得できる Token とClientIDを入力
DISCORD_TOKEN="{Discord Bot Token}"
# BOT CLIENT ID
CLIENT_ID="{Discord Client ID}"
# 接続するサーバーのID
GUILD_ID="{接続するサーバーのID}"
# consoleをn流すchannel
CONSOLE_CHANNEL_ID="{ログ情報をDiscordで確認したい場合のチャンネルID}"
# データベースの設定
SUPABASE_PASS="{database pass}" #これに関してはmemo
SUPABASE_URL="{supabase url}"
SUPABASE_ANON_KEY="{supabase anon key}"
SUPABASE_TABLE_NAME="{table name}"
- main.ts: Discord APIとBotのメインロジックを処理するファイル。DB関連はいったん抜きで
import dotenv from 'dotenv';
import { Client, Events, GatewayIntentBits, ActivityType, CommandInteraction, TextChannel, ChatInputCommandInteraction } from 'discord.js';
import fs from 'fs';
import path from 'path';
dotenv.config();
// 環境変数の型安全な取得関数
function getRequiredEnvVar(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name}が見つかりませんでした。`);
}
return value;
}
// 定数の定義
const token = getRequiredEnvVar('DISCORD_TOKEN');
const TABLE_NAME = getRequiredEnvVar('SUPABASE_TABLE_NAME');
const CONSOLE_CHANNEL_ID = getRequiredEnvVar('CONSOLE_CHANNEL_ID');
// コマンドインターフェース
interface Command {
data: {
name: string;
description: string;
};
execute: (interaction: CommandInteraction) => Promise<void>;
}
// クライアントの初期化
const client = new Client({
intents: [GatewayIntentBits.Guilds]
});
// コマンドを保持するMap
const commands: Map<string, Command> = new Map();
client.once(Events.ClientReady, async (c: Client) => {
if (client.user) {
client.user.setActivity("ここにBotのstateをかける", { type: ActivityType.Playing });
}
console.log(`準備OKです! ${c.user?.tag}がログインしました。`);
});
// コマンド読み込み
async function loadCommands(): Promise<void> {
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file =>
file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.cjs')
);
for (const file of commandFiles) {
try {
const filePath = path.join(commandsPath, file);
const command = await import(filePath);
if (command.default?.data?.name) {
console.log(`${command.default.data.name}を登録します。`);
commands.set(command.default.data.name, command.default);
}
} catch (error) {
console.error(`コマンド読み込み中にエラーが発生しました (${file}):`, error);
}
}
}
// コマンド読み込みの実行
loadCommands();
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isCommand()) return;
const command = commands.get(interaction.commandName);
if (!command) {
console.error(`${interaction.commandName}というコマンドには対応していません。`);
return;
}
try {
await command.execute(interaction as ChatInputCommandInteraction);
} catch (error) {
console.error('コマンド実行中にエラーが発生しました:', error);
const response = {
content: 'コマンド実行時にエラーが発生しました。',
ephemeral: true
};
if (interaction.replied || interaction.deferred) {
await interaction.followUp(response);
} else {
await interaction.reply(response);
}
}
});
client.login(token).catch(error => {
console.error('ログインに失敗しました:', error);
process.exit(1);
});
- deploy-commands.ts: スラッシュコマンドをAPI経由でDiscordに登録するためのファイル。
import { REST, Routes, SlashCommandBuilder, CommandInteraction, RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord.js';
import fs from 'fs';
import path from 'path';
import dotenv from "dotenv";
dotenv.config();
// 環境変数としてapplicationId, guildId, tokenの3つが必要です
const {
CLIENT_ID: applicationId,
DISCORD_TOKEN: token,
GUILD_ID : guildId,
NODE_ENV
} = process.env as { [key: string]: string };
// 環境変数が足りない場合のエラーチェック
if (!applicationId || !token || !(GUILD_DEV_ID || GUILD_MAIN_ID)) {
throw new Error("Missing environment variables: CLIENT_ID, GUILD_ID, or DISCORD_TOKEN");
}
// Commandインターフェースの定義
interface Command {
data: SlashCommandBuilder; // コマンドデータを格納
execute: (interaction: CommandInteraction) => Promise<void>; // コマンドの実行関数
}
// commands配列はRESTPostAPIChatInputApplicationCommandsJSONBody型の配列
const commands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = [];
// 非同期関数を使用
const registerCommands = async (): Promise<void> => {
// commandsフォルダ内の全てのコマンドファイルを読み込む
const commandsPath = path.join(__dirname, 'commands');
if (!fs.existsSync(commandsPath)) {
throw new Error(`Commands directory not found: ${commandsPath}`);
}
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js') || file.endsWith('.cjs'));
for (const file of commandFiles) {
const commandModule = await import(commandsPath + '/' + file);
// デフォルトエクスポートされたコマンドを取得
const command: Command = commandModule.default;
commands.push(command.data.toJSON() as RESTPostAPIChatInputApplicationCommandsJSONBody); // コマンドをJSON形式で追加
}
const rest = new REST({ version: '10' }).setToken(token);
// Discordサーバーにコマンドを登録
try {
await rest.put(
Routes.applicationGuildCommands(applicationId, guildId),
{ body: commands },
);
console.log('サーバー固有のコマンドが登録されました!');
} catch (error) {
console.error('コマンドの登録中にエラーが発生しました:', error);
}
};
registerCommands();
一部開発の中で使っていた変数とか残ってるかもだけど 適宜自分の環境に合わせて削除とかしてください!
4. ファイル構成
プロジェクトのファイル構成は以下のように設定します。
root/
│
├── src/
│ ├── commands/
│ ├── main.ts
│ ├── deploy-commands.ts
│ └── DBClient.ts
├── .env
└── package.json
5. commandsフォルダーの生成 + コマンドテンプレートの作成
commands フォルダを作成し、各コマンドをモジュールとして管理します。また、コマンドのテンプレートとなるファイルを生成します。テンプレートには、コマンドの名前、説明、実行ロジックを含めます。 以下のテンプレートを参考に○○.tsを生成してコマンドを増やそう!
import { SlashCommandBuilder, EmbedBuilder, CommandInteraction, ChatInputCommandInteraction, } from 'discord.js';
const command = {
data: new SlashCommandBuilder()
.setName('{{commandName}}')
.setDescription('{{description}}'),
async execute(interaction : {{ここに反応するときの型が来るので適宜変えてね}}) {
// ここにコマンドの処理を記述します
},
};
export default command;
ここからは割と自分たちでオリジナルのことをすると思うので
一旦基礎はここまでかな
続報を記載できればDB周りの整備が終わり次第DBクライアントの作成から
レベル管理のためのDB設計等かけるといいな
ついでにCloudRunでこのボットをデプロイしてみようの会
編集中