コンテンツにスキップ

チェック、フック、エラー

腕を組んで怒っている Bostie

チェックは、ハンドラーを実行してよいかどうかを判断します。フックは、ハンドラーの前後でコードを実行します。エラーフックは、何かが例外を送出したときに応答します。

チェック

import bostie
from bostie import CheckFailure, Context, Permissions
from bostie.checks import Cooldown, MaxConcurrency, bot_has_permissions, has_permissions

bot = bostie.Bot()
BANNED_USERS = {80351110224678912}


@bot.check
def not_banned(ctx: Context) -> bool:
    if ctx.user.id in BANNED_USERS:
        raise CheckFailure("You are banned from using this bot.")
    return True


async def in_a_guild(ctx: Context) -> bool:
    return ctx.guild_id is not None


@bot.command(
    checks=[
        in_a_guild,
        has_permissions(Permissions.MANAGE_MESSAGES),
        bot_has_permissions(Permissions.MANAGE_MESSAGES),
        Cooldown(2, 60, per="channel"),
        MaxConcurrency(1, per="guild"),
    ]
)
async def clean(ctx: Context, count: int = 20) -> None:
    """Delete recent messages."""
    await ctx.respond(f"Deleting {count} messages.", ephemeral=True)

チェックは、コンテキストを受け取って bool を返す(または await して返す)関数です。False を返すと CheckFailure で実行を拒否します。チェックが独自の CheckFailure を送出することもでき、そのメッセージがユーザーに伝えられます。チェックは、コマンド、グループ、サブコマンド、ユーザーコマンド、メッセージコマンド、エントリーポイントコマンド、コンポーネントやモーダルのルートの checks=[...] 設定に指定します。@router.check は、ルーターのすべてのハンドラーに対するチェックを登録します。Bot もルーターの一種です。

最初に Bot のチェックが実行され、次に外側から順に各ルーターのチェック、次に上位から順にグループのチェック、最後にコマンド自身のチェックが実行されます。最初に拒否された時点で実行は止まります。オートコンプリートではチェックは実行されません。

よく使うチェックは bostie.checks にあります。

チェック 拒否時の例外 拒否される条件
has_permissions(permissions) MissingPermissions ユーザーがそのチャンネルで permissions のいずれかを持っていない、または DM にいる
bot_has_permissions(permissions) BotMissingPermissions Bot がそのチャンネルでそれらのいずれかを持っていない
Cooldown(rate, seconds, per="user") CooldownActive そのキーが直近 seconds 秒以内にすでに rate 回使っている
MaxConcurrency(number, per="global") MaxConcurrencyReached そのキーの実行がまだ number 件続いている
has_role(role) MissingRole ユーザーが ID または名前で指定したロールを持っていない、または DM にいる
has_any_role(*roles) MissingRole ユーザーがどのロールも持っていない
is_owner NotOwner ユーザーが bot.owner_ids に含まれていない

MaxConcurrency のように release(ctx) メソッドも持つチェックオブジェクト(checks.ReleasingCheck)は、ハンドラーがどのように終わっても、終了時に解放されます。

per には "user"、"guild"(DM ではユーザー)、"channel"、"global"、またはコンテキストを受け取って任意のハッシュ可能なキーを返す関数を指定します。権限のチェックは Discord がインタラクションと一緒に送る権限を読むので、キャッシュもリクエストも必要ありません。これらのエラーは不足している Permissions を missing に持ち、CooldownActive は秒単位の retry_after を持ちます。

ロールとオーナー

import bostie
from bostie import Context, ResolvedMember, Router
from bostie.checks import has_any_role, has_role, is_owner

bot = bostie.Bot()
owner = Router()
owner.check(is_owner)
MODERATOR_ROLE = 1335174302269571073


@owner.command
async def reload(ctx: Context, module: str) -> None:
    """Load a module of the bot again."""
    bot.reload(module)
    await ctx.respond(f"Reloaded {module}.", ephemeral=True)


@bot.command(checks=[has_role(MODERATOR_ROLE)])
async def warn(ctx: Context, member: ResolvedMember) -> None:
    """Warn a member."""
    await ctx.respond(f"{member.mention}, please follow the rules.")


@bot.command(checks=[has_any_role("Admin", "Event Host")])
async def announce(ctx: Context, text: str) -> None:
    """Post an announcement."""
    await ctx.respond(text)


bot.include(owner)

has_role と has_any_role は、ロールの ID または名前を受け取ります。ID は Discord がインタラクションと一緒に送るロールと照合されるので、キャッシュは必要ありません。名前はキャッシュされたサーバーのロールから検索されるので、キャッシュを使わない Bot や HTTP のみのアプリでは、ロールを ID で指定してください。MissingRole は要求された roles を持ち、そのメッセージは ID で指定されたロールをメンションするので、Discord ではロール名として表示されます。

is_owner は、括弧を付けずにそのままチェックとして使います。通過させるユーザーは bot.owner_ids に入っています。これは Bot(...) に渡した owner_ids、指定がなければ Bot の起動時にアプリケーションから読み込んだアプリのオーナーです。チームに属するアプリの場合は、チームのオーナーと、招待を承認した管理者と開発者になります。読み取り専用のメンバーは含まれません。

ユーザーに伝えられる内容

CheckFailure を処理するエラーフックがない場合、bostie はそのメッセージでエフェメラルに応答します。メッセージは「You need the Manage Messages permission.」、「The bot needs the Manage Messages permission.」、「in 42 seconds」のような相対タイムスタンプ付きの「Try again」、「This is already running; try again once it is done.」、「You need the Moderator role.」、「Only the bot's owners can use this.」、そして単に False が返された場合は「You cannot use this.」です。

前後のフック

import logging
import time

import bostie
from bostie import (
    AutocompleteContext,
    Context,
    CooldownActive,
    ErrorContext,
    Forbidden,
    NotFound,
)

log = logging.getLogger("my-bot")
bot = bostie.Bot()
started: dict[int, float] = {}


@bot.before_invoke
async def start_timer(ctx: Context) -> None:
    started[ctx.interaction.id] = time.monotonic()


@bot.after_invoke
async def log_duration(ctx: Context) -> None:
    took = time.monotonic() - started.pop(ctx.interaction.id)
    log.info("/%s took %.2f s", ctx.command_name, took)


@bot.command
async def fetch(ctx: Context, message_id: str) -> None:
    """Show a message of this channel."""
    if ctx.channel_id is not None:
        message = await bot.rest.messages.fetch(ctx.channel_id, int(message_id))
        await ctx.respond(message.content or "(no text)")


@fetch.on_error(NotFound, ValueError)
async def no_such_message(ctx: ErrorContext, error: NotFound | ValueError) -> None:
    if not isinstance(ctx, AutocompleteContext):
        await ctx.respond("No message has that id here.", ephemeral=True)


@bot.on_error(Forbidden)
async def missing_access(ctx: ErrorContext, error: Forbidden) -> None:
    if not isinstance(ctx, AutocompleteContext):
        await ctx.respond("I cannot see that channel.", ephemeral=True)


@bot.on_error(CooldownActive)
async def slow_down(ctx: ErrorContext, error: CooldownActive) -> None:
    log.info("%s hit a cooldown, %.0f s left", ctx.user, error.retry_after)
    raise error

@router.before_invoke は、チェックを通過した後、ハンドラーの前に実行される async def hook(ctx) を登録します。@router.after_invoke は、ハンドラーの後に実行されるフックを登録し、ハンドラーが例外を送出した場合も実行されます。これらはルーターのすべてのコマンド、コンポーネントのルート、モーダルのルートで実行されるので、コンポーネントを受け取る可能性のあるフックでは Context だと決めつけないでください。Bot の前フックは最初に、後フックは最後に実行されます。後フックの例外はログに記録され、応答は変わりません。

エラーフック

チェック、コンバーター、前フック、ハンドラーから送出された例外は、エラーフックに渡されます。@x.on_error はすべての Exception に対する async def hook(ctx, error) を登録し、@x.on_error(NotFound, ValueError) はそれらの型とそのサブクラスに対するフックを登録します。x はコマンド、グループ、コンポーネントやモーダルのルート、ルーター、または Bot で、それぞれ例外の型ごとに 1 つのフックを持てます。

bostie は次の順にフックを探し、最初に見つかったものがエラーを処理します。

  1. コマンド、次にそのグループ、最上位のグループ。
  2. コマンドが属するルーター、次にそのルーターを含む各ルーター、Bot まで。

同じ場所の中では、エラーに最も近いクラスのフックが優先されます。たとえば NotFound のフックは HTTPException のフックより優先されます。同じエラーを送出したフックは、上の slow_down のように、この順序で次のフックにエラーを渡します。別のエラーを送出したフックの場合、そのエラーはログに記録されます。

コンテキストの型は ErrorContext で、Context、ComponentContext、ModalContext、AutocompleteContext のいずれかです。オートコンプリートのコンテキストは respond できないので、上の例のように確認してください。

フックが応答せずに戻った場合、bostie は警告をログに記録し、「Something went wrong.」と応答します。エラーを処理するフックがない場合、bostie は CheckFailure にはそのメッセージで応答し、それ以外はトレースバック付きでログに記録してから「Something went wrong.」と応答します。ハンドラーがすでに応答していた場合は何も応答せず、オートコンプリートには候補の空のリストが返されます。

ハンドラー外のエラー

リスナーとタスクには、応答する相手のユーザーがいません。リスナー内の例外は ListenerError イベントとして、タスク内の例外は TaskError として送られます。これらのリスナーがなければ、bostie がログに記録します。イベントとバックグラウンドタスクを参照してください。

例外の型

Discord、ネットワーク、Bot の状態に関して bostie が送出する例外は、すべて BostieError です。引数が不正な場合は、代わりに ValueError または TypeError を送出します。一部のエラーは標準の例外でもあります。StateError は RuntimeError、MissingIntent は ValueError、StartupError は OSError、CommandDefinitionError は TypeError です。コマンドとインタラクションに関するエラーは基底クラス CommandError を、ボイスのエラーは VoiceError を共有します。

例外 送出される条件
HTTPException REST リクエストがエラーステータスで終わった。status、code(JSONErrorCode)、message、errors、生の body を持つ
BadRequest、Forbidden、NotFound、ServerError 400、403、404、5xx
RateLimited 429、または max_rate_limit_wait より長く待つことになるリクエストを bostie が止めた。retry_after、is_global を持つ
InvalidToken REST またはゲートウェイで、Discord がトークンを拒否した
TransportError Discord が応答する前にリクエストが失敗した
UnexpectedResponse Discord はリクエストを受け付けたが、そのレスポンスをデコードできない。raw にレスポンスが入っている
RequestBudgetExhausted 直近の無効なリクエストが多すぎるため、IP を守るために bostie がリクエストを拒否した
GatewayClosed、PrivilegedIntentsRequired、ShardingRequired Discord がゲートウェイを恒久的に閉じた。後の 2 つはコード 4014 と 4011 の場合
ShardNotRunning、GatewayQueueFull このプロセスが実行していないシャードに対するゲートウェイコマンド、または待機中のコマンドが多すぎる
StateError start() より前の bot.rest など、オブジェクトの状態では許されない呼び出し
MissingIntent Bot が要求していないインテントを必要とする機能を使った
MemberRequestRateLimited Discord がメンバーのリクエストを拒否した。retry_after を持つ
StartupError bostie が必要なシステムリソースを用意できなかった
CommandDefinitionError コマンド、グループ、オプションが Discord のルールに違反している。デコレーターが送出する
CommandSyncError 同期の際に Discord がコマンドを拒否した
OptionConversionError オプションまたはターゲットを型ヒントに変換できなかった
InteractionResponded、InteractionNotAnswered、InteractionExpired インタラクションの状態では許されない応答の呼び出し
CheckFailure とそのサブクラス チェックが実行を拒否した

ボイスのエラーはボイスのページにあります。エラーのリファレンスには、すべてのクラスとその属性が載っています。