| name | discord-bot-dev |
| description | Discord.pyを使用したBot開発のベストプラクティスとパターン。スラッシュコマンド、音声処理、イベントハンドリングの実装ガイド |
| compatibility | Designed for GitHub Copilot CLI |
| metadata | {"author":"TalkBot2 Project","version":"1.0","original-source":"https://github.com/syuutaMC"} |
Discord Bot Development Skill
このスキルは、discord.pyを使用したDiscord Bot開発における実装パターンとベストプラクティスを提供します。
スキルの対象
- 対象ファイル:
src/bot.py, src/**/*.py
- 対象タスク: Bot機能の実装、コマンド追加、イベントハンドリング、音声処理
- 前提知識: discord.py 2.0+, async/await, Python 3.9+
開発原則
1. スラッシュコマンド優先
discord.py 2.0以降では、スラッシュコマンド(app_commands)を優先的に使用する。
✅ 推奨パターン:
from discord import app_commands
from discord.ext import commands
import discord
class VoiceBot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
intents.message_content = True
intents.voice_states = True
super().__init__(command_prefix="!", intents=intents)
async def setup_hook(self):
"""Bot起動時の初期化処理"""
await self.tree.sync()
@app_commands.command(name="join", description="ボイスチャンネルに参加します")
@app_commands.describe(channel="参加するボイスチャンネル(省略時は現在のチャンネル)")
async def join(
interaction: discord.Interaction,
channel: Optional[discord.VoiceChannel] = None
):
"""ボイスチャンネルに参加するコマンド"""
target_channel = channel or interaction.user.voice.channel
if not target_channel:
await interaction.response.send_message(
"❌ ボイスチャンネルに接続していません。",
ephemeral=True
)
return
try:
await target_channel.connect()
await interaction.response.send_message(
f"✅ {target_channel.name} に参加しました!"
)
except discord.ClientException as e:
await interaction.response.send_message(
f"❌ 接続に失敗しました: {e}",
ephemeral=True
)
❌ 避けるべきパターン (prefix commands):
@bot.command()
async def join(ctx):
pass
2. Intents設定の最小化
必要なIntentsのみを有効化し、セキュリティとパフォーマンスを向上させる。
✅ 推奨パターン:
import discord
intents = discord.Intents.default()
intents.message_content = True
intents.voice_states = True
intents.guilds = True
intents.presences = False
intents.members = False
bot = commands.Bot(command_prefix="!", intents=intents)
❌ 避けるべきパターン:
intents = discord.Intents.all()
3. 権限チェックの実装
コマンド実行前に必要な権限をチェックし、明確なエラーメッセージを返す。
✅ 推奨パターン:
@app_commands.command(name="clear_queue", description="再生キューをクリアします")
@app_commands.checks.has_permissions(manage_messages=True)
async def clear_queue(interaction: discord.Interaction):
"""キューをクリアするコマンド(メッセージ管理権限が必要)"""
if not interaction.guild:
await interaction.response.send_message(
"❌ このコマンドはサーバー内でのみ使用できます。",
ephemeral=True
)
return
queue.clear()
await interaction.response.send_message("✅ キューをクリアしました。")
@clear_queue.error
async def clear_queue_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
"""権限エラーを適切に処理"""
if isinstance(error, app_commands.MissingPermissions):
await interaction.response.send_message(
"❌ このコマンドを実行する権限がありません。\n"
"必要な権限: メッセージの管理",
ephemeral=True
)
4. 音声処理の実装パターン
音声キューの管理:
import asyncio
from collections import deque
from typing import Optional, Deque
class VoiceQueue:
"""音声再生キューの管理"""
def __init__(self):
self.queue: Deque[bytes] = deque()
self.is_playing: bool = False
self.voice_client: Optional[discord.VoiceClient] = None
async def add(self, audio_data: bytes):
"""キューに音声を追加"""
self.queue.append(audio_data)
if not self.is_playing:
await self.process_queue()
async def process_queue(self):
"""キューを順次処理"""
while self.queue and self.voice_client and self.voice_client.is_connected():
self.is_playing = True
audio_data = self.queue.popleft()
await self.play_audio(audio_data)
while self.voice_client.is_playing():
await asyncio.sleep(0.1)
self.is_playing = False
async def play_audio(self, audio_data: bytes):
"""音声データを再生"""
import io
import discord
audio_source = discord.FFmpegPCMAudio(
io.BytesIO(audio_data),
pipe=True,
options="-f wav"
)
self.voice_client.play(audio_source)
async def cleanup(self):
"""リソースのクリーンアップ"""
self.queue.clear()
self.is_playing = False
if self.voice_client and self.voice_client.is_connected():
await self.voice_client.disconnect()
音声イベントのハンドリング:
@bot.event
async def on_voice_state_update(
member: discord.Member,
before: discord.VoiceState,
after: discord.VoiceState
):
"""ボイスチャンネルの状態変化を検知"""
if member.bot:
return
if before.channel and not after.channel:
if before.channel.members and len(before.channel.members) == 1:
voice_client = discord.utils.get(bot.voice_clients, channel=before.channel)
if voice_client:
await voice_client.disconnect()
5. エラーハンドリング
グローバルエラーハンドラー:
@bot.tree.error
async def on_app_command_error(
interaction: discord.Interaction,
error: app_commands.AppCommandError
):
"""スラッシュコマンドのグローバルエラーハンドラー"""
if isinstance(error, app_commands.CommandOnCooldown):
await interaction.response.send_message(
f"⏱️ クールダウン中です。{error.retry_after:.1f}秒後に再試行してください。",
ephemeral=True
)
elif isinstance(error, app_commands.MissingPermissions):
permissions = ", ".join(error.missing_permissions)
await interaction.response.send_message(
f"❌ 必要な権限がありません: {permissions}",
ephemeral=True
)
elif isinstance(error, app_commands.BotMissingPermissions):
permissions = ", ".join(error.missing_permissions)
await interaction.response.send_message(
f"❌ Botに必要な権限がありません: {permissions}",
ephemeral=True
)
else:
import traceback
print(f"Unexpected error: {error}")
traceback.print_exc()
await interaction.response.send_message(
"❌ コマンドの実行中にエラーが発生しました。",
ephemeral=True
)
6. イベントハンドリングのベストプラクティス
Bot起動イベント:
@bot.event
async def on_ready():
"""Bot起動時の処理"""
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
print(f"Connected to {len(bot.guilds)} guilds")
await bot.change_presence(
activity=discord.Activity(
type=discord.ActivityType.listening,
name="メッセージを読み上げ中"
)
)
メッセージイベント:
@bot.event
async def on_message(message: discord.Message):
"""メッセージ受信時の処理"""
if message.author.bot:
return
if not message.guild:
return
if message.type != discord.MessageType.default:
return
voice_client = message.guild.voice_client
if not voice_client or not voice_client.is_connected():
return
await read_message(message)
実装チェックリスト
コマンド実装時
音声処理実装時
テスト実装時
コード例
完全なコマンド実装例
import discord
from discord import app_commands
from discord.ext import commands
from typing import Optional
import asyncio
from collections import deque
class VoiceBot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
intents.message_content = True
intents.voice_states = True
super().__init__(command_prefix="!", intents=intents)
self.voice_queue: dict[int, deque] = {}
async def setup_hook(self):
"""初期化処理"""
await self.tree.sync()
bot = VoiceBot()
@bot.tree.command(name="join", description="ボイスチャンネルに参加します")
@app_commands.describe(channel="参加するボイスチャンネル(省略時は現在のチャンネル)")
async def join(
interaction: discord.Interaction,
channel: Optional[discord.VoiceChannel] = None
):
"""ボイスチャンネル参加コマンド"""
target_channel = channel or getattr(interaction.user.voice, "channel", None)
if not target_channel:
await interaction.response.send_message(
"❌ ボイスチャンネルに接続していないか、チャンネルを指定してください。",
ephemeral=True
)
return
if interaction.guild.voice_client:
if interaction.guild.voice_client.channel == target_channel:
await interaction.response.send_message(
f"✅ 既に {target_channel.name} に接続しています。",
ephemeral=True
)
return
else:
await interaction.guild.voice_client.move_to(target_channel)
await interaction.response.send_message(
f"🔄 {target_channel.name} に移動しました。"
)
return
try:
await target_channel.connect()
await interaction.response.send_message(
f"✅ {target_channel.name} に参加しました!"
)
except discord.ClientException as e:
await interaction.response.send_message(
f"❌ 接続に失敗しました: {e}",
ephemeral=True
)
@bot.tree.command(name="leave", description="ボイスチャンネルから退出します")
async def leave(interaction: discord.Interaction):
"""ボイスチャンネル退出コマンド"""
if not interaction.guild.voice_client:
await interaction.response.send_message(
"❌ ボイスチャンネルに接続していません。",
ephemeral=True
)
return
if interaction.guild.id in bot.voice_queue:
bot.voice_queue[interaction.guild.id].clear()
await interaction.guild.voice_client.disconnect()
await interaction.response.send_message("👋 ボイスチャンネルから退出しました。")
@bot.event
async def on_ready():
"""Bot起動時"""
print(f"✅ Logged in as {bot.user}")
print(f"📊 Connected to {len(bot.guilds)} guilds")
if __name__ == "__main__":
import os
bot.run(os.getenv("DISCORD_TOKEN"))
参考リソース
関連スキル
- discord-test: Discord Botのテスト作成ガイドライン
- async-error-handling: 非同期エラーハンドリングのベストプラクティス
- commit: Git コミットのワークフロー
更新履歴: