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

Discordボット開発#12コマンドのクールダウン(Cooldown)・権限(Permission)を管理しよう! (Pythonサンプル付き)

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

目次12項目

こんにちは!Mojangcamです 😎

前回の#11では、Flaskでボットの状態を確認できるWebダッシュボードを作りました。ボットの機能はかなりそろってきましたが、実際にサーバーに入れてみると、必ずこんなことが起きます。

誰かが/뽑기を1秒に10回も連打してチャンネルが埋め尽くされたり、誰でも/청소を押せてしまって他人の会話がまるごと消えたり。😱

そこで今回は、コマンドのクールダウン(Cooldown) と 権限(Permission)・ロール(Role)のチェックを学んでいきます。この2つを設定しておくだけで、ボットが一気に「ちゃんと運用されているボット」らしくなりますよ!

🧠 なぜクールダウンと権限が必要なの?

  • スパム防止 : 連打でAPIリクエストが集中すると、Discordのレート制限に引っかかってボットがまるごと動かなくなります。
  • バランス : 出席・くじ引き・経験値のように報酬があるコマンドは、クールタイムがないと意味がありませんよね。
  • 事故防止 : /청소や/밴のようなコマンドを誰でも使えたら、サーバーは1日で崩壊します。
  • エラー対応 : チェックに引っかかったときにボットが何も言わずに黙っていると、ユーザーは「ボット壊れた?」と思ってまた連打します。案内メッセージは必須です。

📌 準備するもの

  • Python 3.8以上 (3.10+推奨)
  • discord.py 2.x — この記事は最新バージョンの 2.7.1 を基準にしています
  • Discordボットのトークン (まだない方は#1 スラッシュコマンド(Slash Command)完全攻略から見てきてください!)
  • テスト用のDiscordサーバー (ロールを自由に作れるところ)
cmake
pip install -U discord.py

バージョンはこうやって確認します。

CSS
python -c "import discord; print(discord.__version__)"
# 2.7.1

⚠️ 1.xバージョンはスラッシュコマンドに対応していません。 先頭が2.になっていない場合は、必ず-Uオプションでアップグレードしてください。

📌 ステップ1 - クールダウン(Cooldown)をかける

スラッシュコマンドにクールタイムをかけるのは、デコレーター1行で終わりです。

Python
import discord
from discord import app_commands
from discord.ext import commands

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

@bot.tree.command(name="출석", description="1日1回の出席チェック!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
    await interaction.response.send_message(
        f"{interaction.user.mention}さん、出席が完了しました! ✅"
    )

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

  • app_commands.checks.cooldown(rate, per, *, key) : スラッシュコマンド専用のクールダウンデコレーターです。
  • rate : per秒の間に何回まで使えるか。上のコードは1なので1回ですね。
  • per : クールタイムの長さ(秒)。60.0なので60秒に1回です。
  • key : 「誰を基準に数えるか」を決める関数です。戻り値はハッシュ可能な値なら何でもOK。
  • key=lambda i: (i.guild_id, i.user.id) : サーバー + ユーザーの組み合わせなので、同じ人でもサーバーが違えばクールタイムは別々にカウントされます。

💡 keyを省略すると、デフォルトはユーザーID基準(全サーバー共通) です。逆にkey=Noneを渡すと、全員で共有するグローバルクールダウンになります。サーバー全体のイベント用コマンドにぴったりですね!

それから、デコレーターの順番が重要です。@bot.tree.command()は必ずいちばん上に置いてください。下に付けると、コマンドとして登録されません。

📌 ステップ2 - クールタイムに引っかかったときに案内する

ステップ1のコードだけを動かすと、クールタイムに引っかかったときにボットは何の反応もしません。ユーザーから見れば、ただの壊れたボットですよね。エラーハンドラーを付けましょう。

Python
@attend.error
async def attend_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,
        )

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

  • @명령어이름.error : そのコマンド専用のエラーハンドラーを登録します。引数は必ず(interaction, error)の2つでなければなりません。
  • app_commands.CommandOnCooldown : クールタイムに引っかかったときに飛んでくる例外です。
  • error.retry_after : あと何秒待てばいいかを教えてくれるfloat値。:.1fで小数点以下1桁にきれいに切りそろえています。
  • ephemeral=True : コマンドを打った本人にだけ見えるメッセージです。チャンネルが案内メッセージで埋まるのを防いでくれます。

💡 ちなみにerror.cooldownで元の設定値(rate、per)も取り出せます。「60秒に1回だけ使えるコマンドです」のような案内を入れたいときに便利です。

📌 ステップ3 - 権限(Permission)をチェックする

今度は危険なコマンドを防いでみましょう。メッセージ削除コマンドは、メッセージの管理権限を持っている人だけが使えるようにするべきですよね。

less
@bot.tree.command(name="청소", description="チャンネルのメッセージをまとめて削除します")
@app_commands.describe(amount="削除するメッセージの数 (1~100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f"🧹 メッセージを{len(deleted)}件削除しました!", ephemeral=True)

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

  • has_permissions(manage_messages=True) : ユーザーがその権限を持っているかを確認します。なければMissingPermissions例外が発生します。
  • bot_has_permissions(manage_messages=True) : ボットがその権限を持っているかを確認します。なければBotMissingPermissions。これを忘れると、削除の途中でエラーになります!
  • default_permissions(manage_messages=True) : 権限のない人には、コマンドを一覧からまるごと非表示にしてくれます。
  • guild_only() : DMでは使えないようにします。
  • app_commands.Range[int, 1, 100] : 入力値を1~100に制限します。Discordが自動で弾いてくれます。

⚠️ default_permissionsだけを信じてはいけません! 公式ドキュメントにも、これは「ヒントにすぎない(serves as a hint)」と明記されています。サーバー管理者がDiscordの設定でこの値を変えられてしまうのです。 なので、本当に防ぎたいなら必ずhas_permissionsを一緒にかけてください。また、default_permissionsはサブコマンドでは無視され、サーバー側の処理なのでエラーハンドラーも呼ばれません。

📌 ステップ4 - ロール(Role)で制限する

権限の代わりに、「運営スタッフ」のようにサーバーで自分で作ったロールで制限したいことも多いですよね。

less
@bot.tree.command(name="공지", description="サーバーにお知らせを送ります")
@app_commands.describe(message="お知らせの内容")
@app_commands.checks.has_any_role("관리자", "운영진")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
    embed = discord.Embed(title="📢 お知らせ", description=message, color=0x5865F2)
    embed.set_footer(text=f"投稿者: {interaction.user.display_name}")
    await interaction.response.send_message(embed=embed)

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

  • has_role("이름") : 特定のロールを1つ要求します。なければMissingRole。
  • has_any_role("A", "B") : 並べたロールのうち1つでもあれば通過。なければMissingAnyRole。
  • ロールは名前(文字列) の代わりにID(整数) でも指定できます。名前は打ち間違いや変更に弱いので、実運用ならIDをおすすめします。
CSS
@app_commands.checks.has_any_role(123456789012345678, 987654321098765432)

💡 ボーナス - VIPはクールタイム免除にする (dynamic_cooldown)

「支援者ロールはクールタイムを短く」といったこともできます。dynamic_cooldownを使えばOKです。

Python
from typing import Optional

VIP_ROLE_ID = 123456789012345678  # クールタイム免除ロールのID

def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
    user = interaction.user
    if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
        return None                          # Noneを返すとクールタイムなし!
    return app_commands.Cooldown(1, 30.0)    # 一般ユーザーは30秒に1回

@bot.tree.command(name="뽑기", description="ランダムくじを引きます")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
    await interaction.response.send_message(f"🎲 結果: **{random.randint(1, 100)}点**!")

ポイントは、Noneを返すとそのユーザーはクールタイムがまるごと免除されるということです。条件に応じてCooldown(rate, per)オブジェクトを自分で作って返せばOKです。

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

Python
# discord.py 2.7.1 / Python 3.8+
import random
import traceback
from typing import Optional

import discord
from discord import app_commands
from discord.ext import commands

TOKEN = "ここにボットトークンを入れてください"
VIP_ROLE_ID = 123456789012345678  # クールタイム免除ロールのID

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

@bot.event
async def on_ready():
    await bot.tree.sync()
    print(f"✅ ログイン完了: {bot.user}")

# ── 1) クールダウン: サーバー別・ユーザー別に60秒に1回 ──────────────────────
@bot.tree.command(name="출석", description="1日1回の出席チェック!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
    await interaction.response.send_message(
        f"{interaction.user.mention}さん、出席が完了しました! ✅"
    )

# ── 2) 権限チェック: メッセージの管理権限を持つ人だけ ──────────────────
@bot.tree.command(name="청소", description="チャンネルのメッセージをまとめて削除します")
@app_commands.describe(amount="削除するメッセージの数 (1~100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f"🧹 メッセージを{len(deleted)}件削除しました!", ephemeral=True)

# ── 3) ロールチェック: '관리자'(管理者)または'운영진'(運営スタッフ)だけ ───────────────────
@bot.tree.command(name="공지", description="サーバーにお知らせを送ります")
@app_commands.describe(message="お知らせの内容")
@app_commands.checks.has_any_role("관리자", "운영진")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
    embed = discord.Embed(title="📢 お知らせ", description=message, color=0x5865F2)
    embed.set_footer(text=f"投稿者: {interaction.user.display_name}")
    await interaction.response.send_message(embed=embed)

# ── 4) ボーナス: VIPはクールタイム免除 ────────────────────────────
def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
    user = interaction.user
    if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
        return None
    return app_commands.Cooldown(1, 30.0)

@bot.tree.command(name="뽑기", description="ランダムくじを引きます")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
    await interaction.response.send_message(f"🎲 結果: **{random.randint(1, 100)}点**!")

# ── 5) グローバルエラーハンドラー (全コマンド共通) ───────────────────
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    if isinstance(error, app_commands.CommandOnCooldown):
        msg = f"⏳ 早すぎます! **{error.retry_after:.1f}秒** 後にもう一度お試しください。"
    elif isinstance(error, app_commands.MissingPermissions):
        msg = "🚫 このコマンドを使う権限がありません。"
    elif isinstance(error, app_commands.BotMissingPermissions):
        msg = "🤖 ボットに権限がありません!サーバー設定 → ロールでボットの権限を確認してください。"
    elif isinstance(error, app_commands.MissingAnyRole):
        msg = "🎭 このコマンドには管理者・運営スタッフのロールが必要です。"
    elif isinstance(error, app_commands.CheckFailure):
        msg = "❌ 実行条件を満たしていません。"
    else:
        msg = "⚠️ 不明なエラーが発生しました。しばらくしてからもう一度お試しください。"
        traceback.print_exception(type(error), error, error.__traceback__)

    if interaction.response.is_done():
        await interaction.followup.send(msg, ephemeral=True)
    else:
        await interaction.response.send_message(msg, ephemeral=True)

bot.run(TOKEN)

💡 全体コードでは、ステップ2の@attend.errorをわざと外しています。 理由はすぐ下の「よくあるエラー」の3番で説明しますね!

📌 実行結果

ボットを起動して、サーバーでひとつずつ試してみるとこう動きます。

まず/출석を打つと、出席完了メッセージが表示されます。では、すぐにもう一度打つと?クールタイムの案内が自分にだけ表示されます。

案内メッセージの下に「これはあなただけに表示されています(이 메시지는 본인만 볼 수 있어요)」と付いていますよね?これがephemeral=Trueの効果です。チャンネルには何の痕跡も残りません。

権限の違いは、コマンド一覧ですぐにわかります。同じサーバー、同じボットなのに、アカウントの権限によってこんなに変わります。

メッセージの管理権限がないアカウントには、/청소がそもそも表示されません。default_permissionsがDiscordサーバー側で弾いてくれるからです。そして、もしサーバー管理者が権限設定を開放してコマンドが見えるようになったとしても、has_permissionsがもう一度止めてくれるので 「🚫 権限がありません」 と表示されます。

同じように、/공지は管理者・運営スタッフのロールがないと 「🎭 ロールが必要です」 で断られ、VIPロールを付けたアカウントで/뽑기を連打すると、クールタイムなしでずっと回せます。

📌 よくあるエラー

1. TypeError: error handler must have 2 parameters

  • 原因 : エラーハンドラー関数の引数の数が2つではありません。selfを付けたり、1つしか受け取っていなかったりすると発生します。
  • 解決 : 必ずasync def handler(interaction, error):のちょうど2つにそろえてください。Cogの中で使う場合はselfを含めて3つになるので、Cogではcog_app_command_errorをオーバーライドしてください。

2. クールタイムに引っかかったのに、ボットが何も言わない

  • 原因 : エラーハンドラーを付けていません。CommandOnCooldownが発生しても、受け止める場所がなければログに残るだけで、ユーザーには何も届きません。
  • 解決 : @bot.tree.errorのグローバルハンドラーを必ず登録してください。(全体コードの5番ブロック)

3. 案内メッセージが2回届く、またはInteractionRespondedエラーが出る

  • 原因 : 個別ハンドラー(@명령어.error)とグローバルハンドラー(@bot.tree.error)を両方登録したためです。discord.pyは個別ハンドラーを実行した後にも、グローバルハンドラーを必ず呼び出します。そのため、すでに応答したインタラクションにもう一度応答しようとしてInteractionRespondedが発生します。
  • 解決 : どちらか一方だけを使ってください。全体コードのようにグローバルハンドラーに統一するのがおすすめです。どうしても両方使いたいなら、グローバルハンドラーの中でif interaction.response.is_done(): returnで抜ければOKです。

4. MissingPermissionsが管理者にも出る

  • 原因 : has_permissionsはチャンネル単位で計算された実際の権限を見ます。チャンネルの権限の上書き(Override)でその権限が拒否されていると、サーバー管理者でも止められることがあります。
  • 解決 : チャンネルの設定 → 権限で、そのロールの権限が ❌ でオフになっていないか確認してください。

5. /청소が実行の途中で止まる

  • 原因 : ボットにメッセージの管理権限がないか、14日以上前のメッセージを削除しようとした場合です。Discordは14日より前のメッセージの一括削除を禁止しています。
  • 解決 : bot_has_permissionsで先に弾いておき、古いチャンネルでは件数を減らしてテストしてください。

💬 まとめ

今回はDiscordボットにクールダウンと権限・ロールのチェックを付けてみました。まとめるとこうなります 🙌

  • checks.cooldown(rate, per, key=...) → スパム防止、keyで基準を自由に
  • checks.has_permissions / bot_has_permissions → ユーザー・ボットの権限を確認
  • checks.has_any_role → サーバーのロールで制限 (ID推奨)
  • default_permissions → 一覧から隠す、ただしヒントにすぎないので単独で使うのは禁止
  • @bot.tree.error → 案内メッセージは1か所でまとめて管理

次回は Discordボットのデータ保存 - JSON vs SQLite を扱います。出席記録やポイントをボットが止まっても残しておくには欠かせない内容です。お楽しみに! 🙌

気になる点やつまずいたところがあれば、コメントで教えてください 💬

📚 Discordボット開発シリーズ

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

👉 あわせて読みたい : Discordボットのエラー解決まとめ - intents・tokenエラーからボットが反応しないときまで!

連載 · 全32回中12回目

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実践ブログ