🚀 UbuntuサーバーでDiscord自動出欠Botを構築する完全ガイド

(Python + Discord API + cron + Ubuntu Server)

🧭 はじめに

この記事では、Ubuntuサーバー上でDiscord Botを使って自動的に出欠確認アンケートを投稿し、
締切時に未回答者をメンションするシステム
を構築する手順を紹介します。

対象読者は:

  • 学生プロジェクトやサークルなどでDiscordを使っている人
  • 定期的にメンバーの予定を集めたい代表者
  • 無料でサーバーを運用したい人

この構成では、Google Apps Script(GAS)ではできなかったDiscord Botの直接操作
Ubuntuサーバー+Pythonで実現します。


🖥️ 環境の概要

項目使用技術
OSUbuntu Server(例:24.04)
言語Python 3.12
BotライブラリDiscord API(requestsモジュール)
スケジュール管理cron(サーバーの定期実行機能)
保存形式JSONファイル(メッセージIDなどを保持)

Botは次の2つのタスクを行います:

  1. 金曜15時:各班チャンネルにアンケートを自動投稿(リアクション付き)
  2. 日曜18時:未回答者を自動検出しメンション通知

🧩 Step 1. Discord Botの準備

  1. Discord Developer Portal にアクセス
  2. 「New Application」をクリックしBotを作成
  3. 「Bot」タブに移動し
    • 「Privileged Gateway Intents」から
      • ✅ Server Members Intent
      • ✅ Message Content Intent(任意)
        をONにして保存
  4. 「TOKEN」をコピー(これはPythonスクリプトで使う)
  5. 「OAuth2 → URL Generator」で以下を選択:
    • Scopes:bot
    • Permissions:Send Messages, Read Message History, Add Reactions
    • 生成されたURLからBotをサーバーに招待

🐍 Step 2. Ubuntu上でPython環境を構築

sudo apt update
sudo apt install -y python3 python3-venv

仮想環境の作成

mkdir ~/discord-bot
cd ~/discord-bot
python3 -m venv venv
source venv/bin/activate

ライブラリのインストール

pip install requests

✉️ Step 3. 金曜15時に自動投稿されるアンケートBot

ファイル名:send_schedule.py

import os, json, requests, urllib.parse, pathlib

DISCORD_BOT_TOKEN = "YOUR_BOT_TOKEN_HERE"

TEAMS = [
    {"name": "車体製作A班", "channel_id": 111111111111111111, "role_id": 999999999999999991},
    {"name": "車体製作B班", "channel_id": 222222222222222222, "role_id": 999999999999999992},
    {"name": "車体製作C班", "channel_id": 333333333333333333, "role_id": 999999999999999993},
]

REACTIONS = ["🌝", "🔥", "💧", "🌳", "🪙"]

BASE_URL = "https://discord.com/api/v10"
BASE_HEADERS = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}", "User-Agent": "EcoRunBot/1.0"}

DATA_DIR = pathlib.Path("data")
DATA_DIR.mkdir(exist_ok=True)
POLL_FILE = DATA_DIR / "last_polls.json"

def send_message(channel_id, text):
    url = f"{BASE_URL}/channels/{channel_id}/messages"
    headers = {**BASE_HEADERS, "Content-Type": "application/json"}
    r = requests.post(url, headers=headers, json={"content": text})
    if r.status_code not in (200, 201):
        print(f"[ERROR] send_message {channel_id} -> {r.status_code} {r.text}")
        return None
    return r.json()["id"]

def add_reactions(channel_id, message_id, emojis):
    for emoji in emojis:
        e = urllib.parse.quote(emoji)
        url = f"{BASE_URL}/channels/{channel_id}/messages/{message_id}/reactions/{e}/@me"
        r = requests.put(url, headers=BASE_HEADERS)
        if r.status_code not in (204, 200):
            print(f"[WARN] reaction {emoji} -> {r.status_code} {r.text}")

def main():
    polls = {}
    for team in TEAMS:
        ch, role, name = team["channel_id"], team["role_id"], team["name"]
        content = (
            f"<@&{role}> 来週の参加可能な日をリアクションで教えて下さい!\n"
            "月曜日:🌝\n火曜日:🔥\n水曜日:💧\n木曜日:🌳\n金曜日:🪙\n"
            "※締切は日曜日の18時まで"
        )
        print(f"[INFO] sending poll to {name} ({ch})")
        mid = send_message(ch, content)
        if mid:
            add_reactions(ch, mid, REACTIONS)
            polls[str(ch)] = mid
    POLL_FILE.write_text(json.dumps(polls, ensure_ascii=False, indent=2))
    print("[INFO] アンケート送信完了")

if __name__ == "__main__":
    main()

このスクリプトは:

  • 各班のチャンネルにメッセージを投稿
  • 5つのリアクション(月〜金)を自動で付与
  • 投稿されたメッセージのIDを data/last_polls.json に保存

⏰ Step 4. 日曜18時に未回答者をメンションするBot

ファイル名:remind_unanswered.py

import json, requests, urllib.parse, pathlib

DISCORD_BOT_TOKEN = "YOUR_BOT_TOKEN_HERE"

TEAMS = [
    {"name": "車体製作A班", "channel_id": 111111111111111111, "role_id": 999999999999999991},
    {"name": "車体製作B班", "channel_id": 222222222222222222, "role_id": 999999999999999992},
    {"name": "車体製作C班", "channel_id": 333333333333333333, "role_id": 999999999999999993},
]

REACTIONS = ["🌝", "🔥", "💧", "🌳", "🪙"]

BASE_URL = "https://discord.com/api/v10"
HEADERS = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}", "User-Agent": "EcoRunBot/1.0"}
POLL_FILE = pathlib.Path("data/last_polls.json")

def list_role_members(guild_id, role_id):
    members = []
    limit, after = 1000, None
    while True:
        url = f"{BASE_URL}/guilds/{guild_id}/members?limit={limit}"
        if after: url += f"&after={after}"
        r = requests.get(url, headers=HEADERS)
        if r.status_code == 403:
            raise PermissionError("Server Members Intentが無効です。")
        batch = r.json()
        members.extend(batch)
        if len(batch) < limit: break
        after = batch[-1]["user"]["id"]
    return {m["user"]["id"] for m in members if str(role_id) in m.get("roles", [])}

def list_reactors(channel_id, message_id, emojis):
    users = set()
    for emoji in emojis:
        e = urllib.parse.quote(emoji)
        r = requests.get(f"{BASE_URL}/channels/{channel_id}/messages/{message_id}/reactions/{e}?limit=100", headers=HEADERS)
        if r.status_code == 200:
            users |= {u["id"] for u in r.json()}
    return users

def post_message(channel_id, text):
    requests.post(f"{BASE_URL}/channels/{channel_id}/messages",
                  headers={**HEADERS, "Content-Type": "application/json"},
                  json={"content": text})

def main():
    polls = json.loads(POLL_FILE.read_text())
    for team in TEAMS:
        ch, role, name = team["channel_id"], team["role_id"], team["name"]
        mid = polls.get(str(ch))
        if not mid:
            continue
        guild_id = requests.get(f"{BASE_URL}/channels/{ch}", headers=HEADERS).json()["guild_id"]
        role_members = list_role_members(guild_id, role)
        reactors = list_reactors(ch, mid, REACTIONS)
        not_answered = sorted(role_members - reactors)
        if not_answered:
            mentions = " ".join(f"<@{uid}>" for uid in not_answered)
            post_message(ch, f"{mentions}\n参加アンケートの回答ができていません。回答願います🙏")
            print(f"[INFO] {name}: {len(not_answered)}名にリマインドを送信")
        else:
            print(f"[INFO] {name}: 全員回答済み")

if __name__ == "__main__":
    main()

⏲️ Step 5. 定期実行(cron設定)

crontab -e

以下を追記:

# 金曜15:00にアンケートを送信
0 15 * * 5 /usr/bin/bash -lc 'cd /home/ubuntu/discord-bot && source venv/bin/activate && python3 send_schedule.py >> cron.log 2>&1'

# 日曜18:00に未回答者を通知
0 18 * * 0 /usr/bin/bash -lc 'cd /home/ubuntu/discord-bot && source venv/bin/activate && python3 remind_unanswered.py >> cron.log 2>&1'

✅ 動作確認ポイント

チェック項目確認方法
BotがサーバーにいるDiscord上で確認
Tokenが正しいsend_schedule.py実行で200が返る
Privileged Intent有効Developer Portal → BotタブでON
cronが動いているcat cron.log で確認
data/last_polls.jsonがあるcat data/last_polls.json

💡 応用編

  • Googleスプレッドシートと連携して班メンバーを動的に管理
  • カスタム絵文字を使用(<:emoji_name:emoji_id> 形式)
  • フォールバック用メンバーIDリストを手動指定
  • Replitなど無料クラウドへの移植も可能

🧾 まとめ

項目内容
使用技術Python + Discord REST API + cron
成果Discord上で自動的に出欠確認を実施
メリットサーバーが無料・安定・再利用可能
拡張性シート連携やカスタム通知も簡単に追加可能

この仕組みにより、
「毎週の出欠確認」「リーダーの手動リマインド」「メンバーの確認漏れ」
といった問題を完全に自動化できます。


📘 最終的な成果:

金曜15時 → 各班にアンケート投稿
日曜18時 → 未回答者を自動メンション通知

すべてUbuntuサーバー上で24時間無料稼働。
人の手を介さず、Discordで自動出欠管理が可能に!

コメント

タイトルとURLをコピーしました