[Dpy]Disocord.pyでエラーハンドリング

この記事は約11分で読めます。

全体のエラーハンドリング

Discord.pyのcommandsを使うと、@bot.eventしたasync def on_command_error()でcommands経由の全エラーを対象に、またon_errorで非Commandsエラーを回収できます(これは標準機能)。

関数ごとのエラーハンドリング

あるいは

from discord.ext import commands
import discord

@bot.commands
async def hoge(ctx):
    処理

@hoge.error
async def hoge_error(ctx, error):
    await ctx.send("エラーが出たよ!")

のように書けば、指定関数でのみエラーを処理し、エラー解説メッセージを出力する等の処理ができます。正規のコマンド例を個別に出すなんてことも出来ますね。

例えば引数不足ならこう。

@hoge.error
async def hoge_error(ctx, error):
if isinstance(error, commands.errors.MissingRequiredArgument ):
    await ctx.send("引数が足りないよ!")

MissingRequiredArgument等のエラー内容一覧は、commandsのリファレンスを参照してください。

InvokeErrorの注意点

作成した関数外でエラーが出た場合はCommandInvokeErrorとなり、error.orignalに本来のエラーが格納されるため、これを取り出して処理します。InvokeError以外ではoriginalが存在しないので、単純にif isinstance(error.original, エラー内容 ):と書くと、Invoke以外で動作しなくなる点に注意してください。

そのため、まずInvokeかどうかを判別し、Invokeであればoriginalを判定、とする必要があります。

if isinstance(error, commands.CommandInvokeError):
    if isinstance(error.original, エラー内容 ):
        await ctx.send("OOエラー(commands外)が発生しました。")
    else:
        await ctx.send("想定外のエラー(commands外)が発生しました。")
elif isinstance(error,  commands.errors.エラー内容 ):
    await ctx.send("XXエラー(commands内)が発生しました")
elif isinstance(error, commands.errors.PrivateMessageOnly):
    return
else:
    await ctx.send("想定外のエラー(commands内)が発生しました。")

関数外のエラー内容は、Discord.py本家のものと、Python組み込みのものを覚えておけば基本的には問題ないでしょう。例えばスプレッドシートを扱うのなら、スプレッドシートapiのエラーもここに入ります。

PrivateMessageOnly(@dm_only違反)のように、何も反応させたくない例外は個別に早期returnで処理します。

 

@hoge.error
async def hoge_error(ctx, error):
print(f"{type(error)=}") # デバッグ用
print(f"{error=}")
print(f"{type(error.original)=}")
print(f"{error.original=}")

としておくとコンソールにエラー種別と内容が出力されるため、開発が楽になります。

実用サンプルコード

ただコマンドごとのエラー作るのめんどくさいですし、基本的にはこれコピペ+送信先とかを自分なりに改変で行けると思います。普段私が動かしているBotから、一部を切り出して持ってきたものです。

error_logch = bot.get_channel(*****)  # この行はon_readyに入れる
perm_dic = {"add_reactions": "リアクションの追加", "administrator": "管理者", "attach_files": "ファイルを添付", "ban_members": "メンバーをBAN", "change_nickname": "ニックネームの変更", "connect": "接続(ボイスチャンネル)", "create_instant_invite": "招待を作成", "deafen_members": "メンバーのスピーカーをミュート", "embed_links": "埋め込みリンク", "external_emojis": "外部の絵文字を使用する", "external_stickers": "外部のスタンプを使用する(Use Ecternal Stickers)", "kick_members": "メンバーをキック", "manage_channels": "チャンネルの管理", "manage_emojis": "絵文字の管理", "manage_emojis_and_stickers": "絵文字・スタンプの管理", "manage_events": "", "manage_guild": "サーバー管理", "manage_messages": "メッセージの管理", "manage_nicknames": "ニックネームの管理", "manage_permissions": "ロールの管理", "manage_roles": "ロールの管理", "manage_threads": "スレッドの管理", "manage_webhooks": "ウェブフックの管理", "mention_everyone": "`@evryone`,`@here`,すべてのロールにメンション", "move_members": "メンバーを移動(ボイスチャンネル)", "mute_members": "メンバーをミュート", "priority_speaker": "優先スピーカー", "read_message_history": "メッセージ履歴を読む", "read_messages": "チャンネルを見る", "request_to_speak": "スピーカー参加権をリクエスト", "send_messages": "メッセージを送信", "send_tts_messages": "テキスト読み上げメッセージを送信する", "speak": "発言(ボイスチャンネル)", "stream": "WEBカメラ(映像を配信する)", "use_external_emojis": "外部の絵文字を使用する", "use_external_stickers": "外部のスタンプを使用する(Use Ecternal Stickers)", "use_private_threads": "非公開スレッドの使用(Private Thread)", "use_slash_commands": "スラッシュコマンドを使用", "use_threads": "公開スレッドの使用(Public Thread)", "use_voice_activation": "音声検出を使用", "value": "", "view_audit_log": "監査ログを表示", "view_channel": "チャンネルを見る", "view_guild_insights": "サーバーインサイトを見る", }

def t_perm(perm):
    if perm in perm_dic:
        return perm_dic[perm]
    else:
        return perm

@bot.event
async def on_command_error(ctx, error):
    async def error_send(ctx, error):
        time = jst().strftime("%Y/%m/%d %H:%M")
        orig_error = getattr(error, "original", error)
        error_msg = "".join(traceback.TracebackException.from_exception(orig_error).format())        
        embed = discord.Embed(description=f'{ctx.author.display_name}({ctx.author.name}#{ctx.author.discriminator})\n{ctx.message.content}', color=0xf04747)
        embed.set_footer(text=f'{ctx.guild.name}(#{ctx.channel.name})\ntime:{time}\nbot.get_channel({ctx.channel.id}).fetch_message({ctx.message.id})')
        await error_logch.send("<@*****>", embed=embed, file=discord.File(io.StringIO(error_msg), "error.py"))
        msg = f'想定外のエラーが発生しました。コマンドが指定形式通りか確認して下さい。コマンドが正しいにも関わらずエラーが発生する場合、IDとともに公式サーバで質問して下さい。\nID: {ctx.message.id}'
        embed = discord.Embed(title='エラーが発生しました', description=msg)
        await ctx.send(embed=embed)
        await ctx.send("https://discord.gg/6VKu7XB7b5")
        print(error_msg)
    if isinstance(error, commands.CommandNotFound):
        return
    elif isinstance(error, commands.MissingPermissions):
        p = "\n".join([t_perm(perm) for perm in error.missing_permissions])
        await ctx.send(f'{p}\nこのコマンドを実行するためには上記の権限が必要です。')
    elif isinstance(error, commands.BotMissingPermissions):
        p = "\n".join([t_perm(perm) for perm in error.missing_permissions])
        await ctx.send(f'{p}上記の権限が不足しているためBotが動作できません。')
    elif isinstance(error, commands.NoPrivateMessage):
        await ctx.author.send("このコマンドはサーバーでのみ使用できます。", delete_after=15)
    elif isinstance(error, commands.PrivateMessageOnly):
        await ctx.author.send("このコマンドはDMでのみ使用できます。", delete_after=15)
    elif isinstance(error, commands.DisabledCommand):
        await ctx.send("このコマンドは製作者により無効化されています。")
    elif isinstance(error, discord.errors.NotFound):
        await ctx.send("もう一度試してみてください。")
    else:
        await error_send(ctx, error)

@bot.event
async def on_error(event, *args, **kwargs):
    time = jst().strftime("%Y/%m/%d %H:%M")
    error_msg = traceback.format_exc()

    if "in MakeThread" in error_msg and "Missing Access" in error_msg:
        await args[0].channel.send("権限不足により、新規スレッド自動作成が実行できません。Botの権限を見直して下さい。")
        return

    ar = "args"
    for arg in args:
        ar += f'\n\n{type(arg)}\n{repr(arg)}'
    ar += "kwargs"
    for kwarg in kwargs:
        ar += f'\n\n{type(kwarg)}\n{repr(kwarg)}'

    embed = discord.Embed(description=f'{event=}', color=0xf04747)
    embed.set_footer(text=f'{ar}\ntime:{time}')
    await error_logch.send("<@*****>", embed=embed, file=discord.File(io.StringIO(error_msg), "error.py"))
    print(error_msg)

コメント

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