カテゴリー: Discordボット開発

Discordボット開発#11FlaskでWebダッシュボードを作る! (Pythonサンプル付き)

この記事は韓国語の原文から翻訳したものです。

目次10項目

こんにちは!Mojangcamです 😎

前回の#10では、ボットをグローバルボットにする多言語(i18n)対応を扱いましたね。本来はクールダウン・権限管理を予告していましたが、その前に多くの方が気になっているテーマを先に持ってきました。そう、Webダッシュボード(Web Dashboard)作りです!(クールダウン編ももうすぐ公開するので、お楽しみに)

ボットをサーバーで動かしていると、こんなことが気になってきます。「今、ボットってちゃんと生きてるかな...?」毎回Discordを開いて確認する代わりに、ブラウザのアドレスバーにURLを入れるだけでボットの状態・サーバー数・Pingがひと目でわかるページがあったら、ずっと便利ですよね?今回はPythonのWebフレームワークFlaskで、そんなミニ管制塔を作っていきます。

🧠 なぜFlaskなの?

Flaskは、Pythonでいちばん有名なマイクロWebフレームワークです。「マイクロ」という名前のとおり、数行のコードでWebサーバーがさっと作れるので、ボットに添えるダッシュボード用にぴったりです。

  • ボットが動いているかをブラウザ(スマホでも!)ですぐ確認
  • サーバー数・ユーザー数・応答速度をリアルタイムで監視
  • あとからコマンド統計、ログビューアーなどに無限に拡張できる

ひとつだけ知っておきたい概念があります。discord.pyは非同期(asyncio)で動くプログラムで、Flaskは同期方式のWebサーバーです。この2つを1つのファイルで一緒に動かすには、Flaskを別のスレッド(Thread)で実行するテクニックが必要です。今回はその方法までしっかり身につけましょう!

📌 準備するもの

  • Python 3.9以上 (Flask 3.1は3.9+が必要)
  • discord.py 2.x (この記事は2.7.1を基準にしています)
  • Flask 3.1.x (この記事は3.1.3を基準にしています)
  • ボットトークン + ボットを招待したテスト用サーバー (#1を参照)
mipsasm
pip install -U discord.py flask

📌 ステップ1 - Flaskサーバーを単体で立ち上げてみる

ボットと組み合わせる前に、Flaskがどんな形をしているのか、5行の最小サンプルで感覚をつかんでみましょう。

Python
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "ボットのダッシュボードを準備中です!"

app.run(host="0.0.0.0", port=5000)

ひとつずつ見ていきましょう 💡

  • Flask(__name__) : Webアプリのオブジェクトを作ります。__name__は「今このファイルがアプリの基準点」という意味です。
  • @app.route("/") : ブラウザが/のアドレス(トップページ)にアクセスしたら、下の関数を実行するという目印です。スラッシュコマンドのデコレーターとまったく同じ仕組みですよね?
  • host="0.0.0.0" : 自分のパソコンだけでなく、同じネットワークにある他の機器(スマホなど)からのアクセスも許可します。自分だけが見るなら127.0.0.1に変えてもOKです。
  • port=5000 : アクセスするアドレスはhttp://localhost:5000になります。

実行してブラウザでhttp://localhost:5000を開くと、メッセージが表示されます。確認できたらCtrl+Cで止めて、次のステップへ!

📌 ステップ2 - ボットとFlaskを1つのファイルで一緒に動かす

ここが今回のいちばん大事なところです。bot.run()もapp.run()も、それぞれプログラム全体をつかんで離さない(ブロッキング)関数なので、そのまま続けて書くとどちらか一方しか実行されません。解決策は、Flaskをデーモンスレッドに分けることです。

コード
import threading

def run_flask():
    app.run(host="0.0.0.0", port=5000, use_reloader=False)

threading.Thread(target=run_flask, daemon=True).start()  # Flaskは別スレッド
bot.run("ここにボットトークンを入力")  # ボットはメインスレッド

ひとつずつ見ていきましょう 💡

  • threading.Thread(...) : Flaskサーバーをボットと同時に動かすための、別の処理の流れです。
  • daemon=True : メインプログラム(ボット)が終了すると、Flaskも一緒に終了するようにします。これがないと、ボットを止めてもFlaskが残ってターミナルが閉じません。
  • use_reloader=False : Flaskの自動リロード機能をオフにします。この機能はメインスレッドでしか動かないので、オンのままだとスレッド内でエラーになります。(よくあるエラーの1番を参照!)
  • 順番に注意 : bot.run()はシグナル処理の関係で、必ずメインスレッドに置く必要があります。Flaskをスレッドに分ける理由はこれです。

📌 ステップ3 - ボットの状態API + ダッシュボードページを作る

いよいよ、ボットの情報をJSONで返すAPIと、それを5秒ごとに受け取って表示するHTMLページを作ります。

mipsasm
from flask import jsonify

@app.route("/api/stats")
def stats():
    return jsonify({
        "online": bot.is_ready(),
        "name": str(bot.user) if bot.user else "接続中...",
        "servers": len(bot.guilds),
        "members": sum(g.member_count or 0 for g in bot.guilds),
        "ping_ms": round(bot.latency * 1000) if bot.is_ready() else None,
    })

ひとつずつ見ていきましょう 💡

  • jsonify(...) : Pythonの辞書を、ブラウザが読めるJSONレスポンスに変換してくれます。
  • bot.is_ready() : ボットがDiscordに完全にログインしたかどうかです。ダッシュボードのオンライン/オフライン表示に使います。
  • bot.guilds : ボットが参加しているサーバーの一覧です。len()でサーバー数を数えます。
  • g.member_count : サーバーごとのメンバー数です。デフォルトのインテントでも読み取れます (membersインテントは別途不要)。
  • bot.latency : Discordとの応答の遅延です。秒単位の小数なので、×1000でmsに変換します。ログイン前は値がない(nan)ので、is_ready()で一度ふるいにかけます。

ダッシュボードの画面(HTML)は全体のコードに入れてあります。fetch()で5秒ごとに/api/statsを呼び出して数字を更新する、シンプルな構造です。

📌 全体のコード (コピペOK)

XML
# discord.py 2.7.x / Flask 3.1.x / Python 3.9+
import threading

import discord
from discord.ext import commands
from flask import Flask, jsonify, render_template_string

intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
app = Flask(__name__)

PAGE = """
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ボットダッシュボード</title>
<style>
  body { font-family: sans-serif; background: #313338; color: #fff;
         display: flex; flex-direction: column; align-items: center; }
  h1 { margin-top: 40px; }
  .cards { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; }
  .card { background: #2b2d31; border-radius: 12px; padding: 24px 32px;
          text-align: center; min-width: 140px; }
  .card .num { font-size: 32px; font-weight: bold; color: #5865f2; }
  .on  { color: #23a55a; }
  .off { color: #f23f43; }
</style>
</head>
<body>
  <h1>🤖 <span id="name">読み込み中...</span></h1>
  <p id="status" class="off">● 確認中</p>
  <div class="cards">
    <div class="card"><div class="num" id="servers">-</div>サーバー</div>
    <div class="card"><div class="num" id="members">-</div>ユーザー</div>
    <div class="card"><div class="num" id="ping">-</div>Ping(ms)</div>
  </div>
  <script>
    async function refresh() {
      const res = await fetch("/api/stats");
      const d = await res.json();
      document.getElementById("name").textContent = d.name;
      const s = document.getElementById("status");
      s.textContent = d.online ? "● オンライン" : "● オフライン";
      s.className = d.online ? "on" : "off";
      document.getElementById("servers").textContent = d.servers;
      document.getElementById("members").textContent = d.members;
      document.getElementById("ping").textContent = d.ping_ms ?? "-";
    }
    refresh();
    setInterval(refresh, 5000);  // 5秒ごとに自動更新
  </script>
</body>
</html>
"""

@app.route("/")
def dashboard():
    return render_template_string(PAGE)

@app.route("/api/stats")
def stats():
    return jsonify({
        "online": bot.is_ready(),
        "name": str(bot.user) if bot.user else "接続中...",
        "servers": len(bot.guilds),
        "members": sum(g.member_count or 0 for g in bot.guilds),
        "ping_ms": round(bot.latency * 1000) if bot.is_ready() else None,
    })

def run_flask():
    app.run(host="0.0.0.0", port=5000, use_reloader=False)

@bot.event
async def on_ready():
    print(f"{bot.user} ログイン完了!")
    print("ダッシュボード: http://localhost:5000")

threading.Thread(target=run_flask, daemon=True).start()
bot.run("ここにボットトークンを入力")

📌 実行結果

ボットを起動すると、ターミナルにログインメッセージとダッシュボードのアドレスが表示されます。ブラウザでhttp://localhost:5000を開くと...

  • ボットの名前と、緑色の「● オンライン」が表示されます
  • サーバー数 / ユーザー数 / Ping(ms) がカードで表示され、5秒ごとに自動更新されます
  • ボットを止めると(ダッシュボードだけ動いているとき)、赤い「● オフライン」に変わります
  • 同じWi-Fiにつないだスマホからhttp://내컴퓨터IP:5000(「내컴퓨터IP」の部分は自分のPCのIPアドレス)にアクセスすれば、スマホでも確認できます!

📌 よくあるエラー

  1. ValueError: signal only works in main thread 原因: Flaskのデバッグモード(自動リロード)をオンにしたままスレッドで実行したか、逆にbot.run()をスレッドに回した場合です。どちらも、メインスレッドでないとシグナル処理で落ちます。 解決: app.run(..., use_reloader=False)を必ず入れて、ボットはメインスレッド / Flaskはスレッドという構成を守ってください。
  2. OSError: [Errno 98] Address already in use (Windows: WinError 10048) 原因: 5000番ポートを、すでに別のプログラム(または終了していない前回の実行)が使っています。 解決: port=5001のように別のポートに変えるか、ターミナルを完全に閉じてからもう一度実行してください。
  3. ダッシュボードにサーバー0個・ユーザー0人と表示される 原因: ボットがまだログイン中か、ボットが招待されたサーバーがない場合です。 解決: 少し待ってから再読み込みしてみてください(5秒ごとに自動更新されます)。ずっと0のままなら、ボットの招待リンクでテストサーバーに招待されているか確認してください。

📌 まとめ

今回はFlaskを別スレッドで動かして、ボットとWebサーバーを1つのファイルで同時に実行し、ボットの状態・サーバー数・Pingを表示するリアルタイムWebダッシュボードを作ってみました。ここにコマンドの使用統計や最近のログを追加すれば、本格的な管理者ページに育てられますよ!

次回は、コマンドの連打を防ぐクールダウン(Cooldown)と権限管理を扱う予定です。お楽しみに 🙌

📚 Discordボット開発シリーズ

  1. Discordボット開発 - スラッシュコマンド(Slash Command)完全攻略!
  2. Discordボット開発 #2 - スラッシュコマンドにオプション(Arguments)を追加しよう!
  3. Discordボット開発 #3 - スラッシュコマンドにサブコマンド(Subcommand)を追加しよう!
  4. Discordボット開発 #4 - スラッシュコマンドで埋め込み(Embed)を送信しよう!
  5. Discordボット開発 #5 - ボタン(Button)のインタラクションを実装しよう
  6. Discordボット開発 #6 - ドロップダウン(Select Menu)メニューを実装しよう
  7. Discordボット開発 #7 - モーダル(Modal)で入力を受け取る
  8. Discordボット開発 #8 - 埋め込み(Embed)を画像・フィールドで飾る
  9. Discordボット開発 #9 - AI(Claude・ChatGPT API)と連携する
  10. Discordボット開発 #10 - 多言語(i18n)に対応する
  11. Discordボット開発 #11 - FlaskでWebダッシュボードを作る ← 今読んでいる記事!

連載 · 全32回中11回目

Discordボット開発

  1. #1Discordボット開発 - スラッシュコマンド(Slash Command)完全攻略!
  2. #2スラッシュコマンドにオプション(Arguments)を追加しよう!
  3. #3スラッシュコマンドにサブコマンド(Subcommand)を追加しよう!
  4. #4スラッシュコマンドで埋め込み(Embed)を送信しよう!
  5. #5ボタン(Button)のインタラクションを実装しよう!
  6. #6ドロップダウン(Select Menu)メニューを実装しよう!
  7. #7モーダル(Modal)で入力を受け取る方法 (Pythonサンプル付き)
  8. #8埋め込み(Embed)を画像・フィールドで飾る (Pythonサンプル付き)
  9. #9AI(Claude・ChatGPT API)と連携する! (Pythonサンプル付き)
  10. #10多言語(i18n)に対応する! (Pythonサンプル付き)
  11. #11FlaskでWebダッシュボードを作る! (Pythonサンプル付き)現在の記事
  12. #12コマンドのクールダウン(Cooldown)・権限(Permission)を管理しよう! (Pythonサンプル付き)
  13. #13ロール・チャンネルの自動化でサーバーを管理しよう! (Pythonサンプル付き)
  14. #14データを保存しよう!JSON vs SQLite (Pythonサンプル付き)
  15. #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제) (韓国語)
  16. #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제) (韓国語)
  17. #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제) (韓国語)
  18. #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제) (韓国語)
  19. #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제) (韓国語)
  20. #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제) (韓国語)
  21. #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제) (韓国語)
  22. #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제) (韓国語)
  23. #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제) (韓国語)
  24. #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제) (韓国語)
  25. #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제) (韓国語)
  26. #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제) (韓国語)
  27. #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제) (韓国語)
  28. #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제) (韓国語)
  29. #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제) (韓国語)
  30. #30投票・アンケート(Poll)ボットを作ろう!ネイティブ投票+ボタンでリアルタイム集計まで(Pythonサンプル) 🗳️
  31. #32サーバーログ・監査ログ(Audit Log)チャンネルを作ろう!メッセージの削除・編集・入退室・キックまで自動記録(Pythonサンプル) 🕵️
  32. #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (韓国語)
全32回を表示閉じる

著者

Mojangcam

自分でコードを書き、サーバーを動かし、詰まったエラーを解決しながら学んだことを記録する開発・IT実践ブログ