2025/02/01 / Discord

Discord Bot のアーキテクチャを見直した話

Discord Bot の構成メモ コマンド/イベントをプラグイン的に追加

discord nodejs architecture plugin tool

こんにちは!パン君です。

この記事では、個人で運用している Discord Bot のアーキテクチャをどう組み直したか をまとめます。

やりたかったことはざっくりいうと、

  • コマンドやイベントを「 ファイルを追加するだけ 」で拡張できる
  • サーバー上では pm2 で常駐管理
  • git pull +シェルスクリプトで 半自動デプロイ
  • 将来的に「管理用 Web ページ」や「API」とも連携しやすくしておく

という構成です。


目標と前提

目標

  1. Bot 本体をコア/プラグインに分離
    • コマンドやイベントは「後から足せるモジュール」として扱う。
  2. 運用を楽にする
    • Ubuntu サーバー上で pm2 にお任せして、SSH を切っても動き続ける。
  3. デプロイをシンプルにする
    • 管理用リポジトリに push → サーバー側で pull +再起動するだけ。

前提

  • 言語:Node.js(TypeScript も見据えつつ、現在は主に JS)
  • ライブラリ:discord.js
  • 実行環境:Ubuntu Server + pm2
  • 外部アクセス:playit.gg でトンネルを張って、外部から接続可能

ディレクトリ構成

最終的に落ち着いた構成はこんな感じです。

Text
bot-root/
├── Command/        # コマンドモジュール
├── Event/          # イベントハンドラ
├── Data/           # JSON 等の設定・永続データ
├── Class/          # 共通クラス・ヘルパ
├── Shell/          # デプロイ用シェルスクリプト
├── types/          # JSDoc など型関連
├── logs/           # pm2 のログ出力先
├── config.json     # トークンや設定(dotenv でも可)
└── index.js        # エントリポイント(コア)

ポイントは Command / Event / Data を完全に分離 したことです。

  • index.js(コア)は「モジュールを読み込んでWiringするだけ」
  • コマンドの中身は Command/ 下のJSファイルが担当
  • イベントは Event/ 下のJSファイルが担当 という形にすることで、 Bot本体を触らずに機能追加 ができるようになりました。

コマンドのプラグイン構造

コマンドモジュールの形

各コマンドは大体こんな形のJSファイルを1ファイルで持ちます。

JavaScript
// Command/ping.js
/** @type {import('../types').BotCommand} */
module.exports = {
  name: "ping",
  description: "疎通確認用コマンド",
  options: [],
  async execute(interaction, context) {
    const start = Date.now();
    await interaction.reply("Pong!");
    const ms = Date.now() - start;
    console.log(`[ping] latency: ${ms}ms`);
  },
};
  • name / description / options はそのまま slash command の定義に流用
  • execute には interaction と共通の context(設定・ロガーなど)を渡す

動的読み込み

コア側では、起動時に Command/ ディレクトリを走査して全部読み込みます。

やっていることはシンプルで、

  1. fs.readdirSync("Command") で .js ファイル一覧を取得
  2. require して name をキーに Map に登録
  3. slash command の登録用に JSON を組み立てる という流れです。

このおかげで、新しいコマンドを追加するときは

  1. Command/ に JS ファイルを作る
  2. Git に commit + push
  3. サーバーで pull して再起動 だけで済むようになりました。

イベントハンドラの構造

イベントもコマンドと同じノリでファイルベースにしています。

JavaScript
// Event/ready.js
/** @type {import('../types').BotEvent} */
module.exports = {
  name: "ready",
  once: true,
  execute(client, context) {
    console.log(`[ready] Logged in as ${client.user.tag}`);
  },
};
JavaScript
// Event/interactionCreate.js
module.exports = {
  name: "interactionCreate",
  once: false,
  async execute(interaction, context) {
    if (!interaction.isChatInputCommand()) return;

    const command = context.commands.get(interaction.commandName);
    if (!command) return;

    try {
      await command.execute(interaction, context);
    } catch (err) {
      console.error(err);
      if (!interaction.replied) {
        await interaction.reply({
          content: "エラーが発生しました。",
          ephemeral: true,
        });
      }
    }
  },
};

コア側では、Event/ を走査して
client.on(event.name, handler) or client.once(...)
を自動で登録するだけにしています。


pm2とデプロイフロー

pm2での常駐

Ubuntu Server 上では pm2 を使って常駐させています。

Bash
pm2 start index.js --name bread-bot
pm2 save
pm2 startup
  • SSH を切っても Bot は動き続ける
  • pm2 logs bread-bot でログ確認
  • pm2 restart bread-bot で反映

という基本の運用です。

デプロイ用スクリプト

デプロイはシンプルに Shell/ に 1 本スクリプトを置きました。

Bash
#!/usr/bin/env bash
set -eu

cd /opt/bread-bot

echo "[deploy] git pull"
git pull origin main

echo "[deploy] npm install"
npm install --production

echo "[deploy] restart pm2"
pm2 restart bread-bot
  • リポジトリを更新 → サーバー側でこのスクリプト実行
  • 将来的には Webhook や CI から叩いてもよい

という形にしています。


これからやりたい拡張

現状でも「コマンドを JS 1 ファイルで追加できる Bot」になりましたが、さらにやりたいのはこのあたりです。

  1. 型まわりの強化
  • TypeScript 化 or JSDoc で interaction / context に型をつける
  • ApplicationCommandOptionData を定義専用のヘルパにまとめる
  1. 管理用 Web ページとの連携
  • Express / Fastify などで API を立てて
  • 「今登録されているコマンド一覧」や「Guild 状態」を Web から確認

3.ログとメトリクス

  • エラーや特定イベントを別チャンネルに送る
  • Cloud 上のログ基盤に送るところまで視野に入れる

まとめ

  • コマンド/イベントを ディレクトリ単位で分割して、Bot 本体は「読み込んで繋ぐだけ」にする
  • pm2 と簡単なシェルスクリプトで、運用とデプロイをシンプルにする
  • 将来の拡張(Web 管理画面や API)を見据えて、context という共通オブジェクトをコマンドに渡す

こういう形にしたことで、 「1つの巨大な index.js に全部書いてある Discord Bot」から、だいぶ卒業できた気がします。

同じように Bot を長期運用したい人の参考になれば嬉しいです。


← Odin でコンテキストエ…← ブログ一覧へ戻るModoium Remot… →