コマンド

コマンド拡張の最も魅力的な機能の一つは、簡単にコマンドが定義でき、かつそのコマンドを好きなようにネスト状にして、豊富なサブコマンドを用意することができる点です。

コマンドは、Pythonの関数と関連付けすることによって定義され、同様のシグネチャを使用してユーザーに呼び出されます。

警告

コマンド拡張機能が機能するには、 message_content インテントが必要です。これは、デベロッパーポータルとコード内の両方で有効化しないといけません。

そうしない場合は、ボットはコマンドに応答しなくなります。

例えば、次のコマンド定義を使うと次のようになります。

@bot.command()
async def foo(ctx, arg):
    await ctx.send(arg)

接頭辞を ($) としたとすると、このコマンドは次のように実行できます。

$foo abc

コマンドには、少なくとも Context を渡すための引数 ctx が必要です。

コマンドを登録するには二通りの方法があります。一つ目は Bot.command() を使用する方法で、二つ目が command() デコレータを使用して Bot.add_command() でインスタンスにコマンドを追加していく方法です。

本質的に、これら2つは同等になります:

import discord
from discord.ext import commands

intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(command_prefix='$', intents=intents)

@bot.command()
async def test(ctx):
    pass

# or:

@commands.command()
async def test(ctx):
    pass

bot.add_command(test)

Bot.command() が簡単かつ理解がしやすいので、ドキュメント上ではこちらを使っています。

Command のコンストラクタの引数はデコレータに渡すことで利用できます。例えば、コマンドの名前を関数以外のものへと変更したい場合は以下のように簡単に設定することができます。

@bot.command(name='list')
async def _list(ctx, arg):
    pass

パラメータ

Pythonの関数定義によって、同時にコマンドを定義するので、関数のパラメータを設定することにより、コマンドの引数受け渡し動作も定義することができます。

特定のパラメータタイプはユーザーサイドで異なる動作を行い、そしてほとんどの形式のパラメータタイプがサポートされています。

位置パラメータ

最も基本的な引数は位置パラメータです。与えられた値をそのまま渡します。

@bot.command()
async def test(ctx, arg):
    await ctx.send(arg)

Botの使用者側は、通常の文字列を渡すだけで位置パラメータに値を渡すことができます。

../../_images/positional1.png

間に空白を含む文字列を渡す場合は、文字列を引用符で囲む必要があります。

../../_images/positional2.png

引用符を用いなかった場合、最初の文字列のみが渡されます。

../../_images/positional3.png

位置パラメータは、Pythonの引数と同じものなので、好きなだけ設定することが可能です。

@bot.command()
async def test(ctx, arg1, arg2):
    await ctx.send(f'You passed {arg1} and {arg2}')

可変長引数

場合によっては、可変長のパラメータを設定したい場合もあるでしょう。このライブラリはPythonの可変長パラメータと同様にこれをサポートしています。

@bot.command()
async def test(ctx, *args):
    arguments = ', '.join(args)
    await ctx.send(f'{len(args)} arguments: {arguments}')

これによって一つ、あるいは複数の引数を受け取ることができます。ただし、引数を渡す際の挙動は位置パラメータと同様のため、複数の単語を含む文字列は引用符で囲む必要があります。

例えば、ボット側ではこのように動きます。

../../_images/variable1.png

複数単語の文字列を渡す際は、引用符で囲んでください。

../../_images/variable2.png

Pythonの振る舞いと同様に、ユーザーは引数なしの状態を渡すことも一応できます。

../../_images/variable3.png

args 変数は tuple となるため、通常のタプルと同様の操作が行なえます。

キーワード引数

引数の構文解析を自分で行う場合や、複数単語の入力を引用符で囲む必要のないようにしたい場合は、渡された値を単一の引数として受け取るようにライブラリに求めることができます。以下のコードのようにキーワード引数のみを使用することでこれが可能になります。

@bot.command()
async def test(ctx, *, arg):
    await ctx.send(arg)

警告

解析が曖昧になるため、キーワード引数は一つまでしか扱えません。

ボット側では、スペースを含む入力を引用符で囲む必要がありません:

../../_images/keyword1.png

引用符で囲んだ場合、消えずに残るので注意してください:

../../_images/keyword2.png

通常、キーワード引数は利便性のために空白文字で分割されます。この動作はデコレータの引数として Command.rest_is_raw を使うことで切り替えることが可能です。

呼び出しコンテキスト

前述の通り、すべてのコマンドは必ず Context と呼ばれるパラメータを受け取らなければいけません。

このパラメータにより、「呼び出しコンテキスト」というものにアクセスできます。言うなればコマンドがどのように実行されたのかを知るのに必要な基本的情報です。これにはたくさんの有用な情報が含まれています。

コンテキストは abc.Messageable インタフェースを実装しているため、 abc.Messageable 上でできることは Context 上でも行うことが可能です。

コンバーター

ボットの引数を関数のパラメータとして設定するのは、ボットのコマンドインタフェースを定義する第一歩でしかありません。引数を実際に扱うには、大抵の場合、データを目的の型へとと変換する必要があります。私達はこれを コンバーター と呼んでいます。

コンバーターにはいくつかの種類があります:

  • 引数を単独のパラメータとして受け取り、異なる型として返す、通常の呼び出し可能オブジェクト。

    • これらにはあなたの作った関数、 boolint といったものまで含まれます。

  • Converter を継承したカスタムクラス。

基本的なコンバーター

基本的なコンバーターは、中核をなすものであり、受け取った引数を別のものへと変換します。

例えば、二つの値を加算したい場合、コンバーターを指定することにより、受け取った値を整数型へ変換するように要求できます。

@bot.command()
async def add(ctx, a: int, b: int):
    await ctx.send(a + b)

コンバーターの指定には関数アノテーションというもの用います。これは PEP 3107 にて追加された Python 3 にのみ実装されている機能です。

これは、文字列をすべて大文字に変換する関数などといった、任意の呼び出し可能関数でも動作します。

def to_upper(argument):
    return argument.upper()

@bot.command()
async def up(ctx, *, content: to_upper):
    await ctx.send(content)

論理型

他の基本的なコンバーターとは違って、 bool のコンバーターは若干扱いが異なります。 bool 型に直接キャストして、空でない引数を True と判断するのではなく、与えられた値に基づいて TrueFalse かを評価します。

if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
    return True
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
    return False

応用的なコンバーター

場合によっては、基本的なコンバーターを動かすのに必要な情報が不足していることがあります。例えば、実行されたコマンドの Message から情報を取得したい場合や、非同期処理を行いたい場合です。

そういった用途のために、このライブラリは Converter インタフェースを提供します。これによって Context にアクセスが可能になり、また、呼び出し可能関数を非同期にもできるようになります。このインタフェースを使用して、カスタムコンバーターを定義したい場合は Converter.convert() をオーバーライドしてください。

コンバーターの例

import random

class Slapper(commands.Converter):
    async def convert(self, ctx, argument):
        to_slap = random.choice(ctx.guild.members)
        return f'{ctx.author} slapped {to_slap} because *{argument}*'

@bot.command()
async def slap(ctx, *, reason: Slapper):
    await ctx.send(reason)

コンバーターはインスタンス化されていなくても構いません。以下の例の二つのは同じ処理になります。

@bot.command()
async def slap(ctx, *, reason: Slapper):
    await ctx.send(reason)

# is the same as...

@bot.command()
async def slap(ctx, *, reason: Slapper()):
    await ctx.send(reason)

コンバーターをインスタンス化する可能性がある場合、コンバーターの調整を行うために __init__ で何かしらの状態を設定することが出来ます。この例としてライブラリに実際に存在する clean_content があります。

@bot.command()
async def clean(ctx, *, content: commands.clean_content):
    await ctx.send(content)

# or for fine-tuning

@bot.command()
async def clean(ctx, *, content: commands.clean_content(use_nicknames=False)):
    await ctx.send(content)

コンバーターが渡された引数を指定の型に変換できなかった場合は BadArgument を送出しないといけません。

埋込み型の応用的なコンバーター

Converter を継承したくない場合のために、応用的なコンバーターの高度な機能を備えたコンバーターを提供しています。これを使用することで2つのクラスを作成する必要がなくなります。

例えば、一般的な書き方だと、クラスとそのクラスへのコンバーターを定義します:

class JoinDistance:
    def __init__(self, joined, created):
        self.joined = joined
        self.created = created

    @property
    def delta(self):
        return self.joined - self.created

class JoinDistanceConverter(commands.MemberConverter):
    async def convert(self, ctx, argument):
        member = await super().convert(ctx, argument)
        return JoinDistance(member.joined_at, member.created_at)

@bot.command()
async def delta(ctx, *, member: JoinDistanceConverter):
    is_new = member.delta.days < 100
    if is_new:
        await ctx.send("Hey you're pretty new!")
    else:
        await ctx.send("Hm you're not so new.")

これでは面倒なので、 classmethod() を使って組み込み型の応用的なコンバーターの実装が可能です。

class JoinDistance:
    def __init__(self, joined, created):
        self.joined = joined
        self.created = created

    @classmethod
    async def convert(cls, ctx, argument):
        member = await commands.MemberConverter().convert(ctx, argument)
        return cls(member.joined_at, member.created_at)

    @property
    def delta(self):
        return self.joined - self.created

@bot.command()
async def delta(ctx, *, member: JoinDistance):
    is_new = member.delta.days < 100
    if is_new:
        await ctx.send("Hey you're pretty new!")
    else:
        await ctx.send("Hm you're not so new.")

Discord コンバーター

Discordモデル を使用して作業を行うのは、コマンドを定義する際には一般的なことです。そのため、このライブラリでは簡単に作業が行えるようになっています。

例えば、 Member を受け取るには、これをコンバーターとして渡すだけです。

@bot.command()
async def joined(ctx, *, member: discord.Member):
    await ctx.send(f'{member} joined on {member.joined_at}')

このコマンドが実行されると、与えられた文字列を Member に変換して、それを関数のパラメーターとして渡します。これは文字列がメンション、ID、ニックネーム、ユーザー名 + Discordタグ、または普通のユーザー名かどうかをチェックすることで機能しています。デフォルトで定義されているコンバーターは、できるだけ簡単に使えるように作られています。

Discordモデルの多くがコンバーターとして動作します。

これらをコンバーターとして設定すると、引数を指定した型へとインテリジェントに変換します。

これらは 応用的なコンバーター インタフェースによって実装されています。コンバーターとクラスの関係は以下の通りです。

コンバーターを継承することで、他のコンバーターの一部として使うことができます:

class MemberRoles(commands.MemberConverter):
    async def convert(self, ctx, argument):
        member = await super().convert(ctx, argument)
        return [role.name for role in member.roles[1:]] # Remove everyone role!

@bot.command()
async def roles(ctx, *, member: MemberRoles):
    """Tells you a member's roles."""
    await ctx.send('I see the following roles: ' + ', '.join(member))

特殊なコンバーター

コマンド拡張機能は一般的な線形解析を超える、より高度で複雑な使用法に対応するため、特殊なコンバーターをサポートしています。これらのコンバーターは、簡単な方法でコマンドに更に容易で動的な文法の導入を可能にします。

typing.Union

typing.Union はコマンドが単数の型の代わりに、複数の特定の型を取り込める特殊な型ヒントです。例えば:

import typing

@bot.command()
async def union(ctx, what: typing.Union[discord.TextChannel, discord.Member]):
    await ctx.send(what)

what パラメータには discord.TextChannel コンバーターか discord.Member コンバーターのいずれかが用いられます。これは左から右の順で変換できるか試行することになります。最初に渡された値を discord.TextChannel へ変換しようと試み、失敗した場合は discord.Member に変換しようとします。すべてのコンバーターで失敗した場合は BadUnionArgument というエラーが発生します。

以前に説明した有効なコンバーターは、すべて typing.Union にわたすことが可能です。

typing.Optional

typing.Optional は「後方参照」のような動作をする特殊な型ヒントです。コンバーターが指定された型へのパースに失敗した場合、パーサーは代わりに None または指定されたデフォルト値をパラメータに渡したあと、そのパラメータをスキップします。次のパラメータまたはコンバータがあれば、そちらに進みます。

次の例をみてください:

import typing

@bot.command()
async def bottles(ctx, amount: typing.Optional[int] = 99, *, liquid="beer"):
    await ctx.send(f'{amount} bottles of {liquid} on the wall!')
../../_images/optional1.png

この例では引数を int に変換することができなかったので、デフォルト値である 99 を代入し、パーサーは処理を続行しています。この場合、先程の変換に失敗した引数は liquid パラメータに渡されます。

注釈

このコンバーターは位置パラメータでのみ動作し、可変長パラメータやキーワードパラメータでは機能しません。

typing.Literal

バージョン 2.0 で追加.

typing.Literal は、渡されたパラメータが同じ型に変換された後にリストされた値のいずれかに等しいことを要求する特別な型ヒントです。 例えば、以下のように指定します。

from typing import Literal

@bot.command()
async def shop(ctx, buy_sell: Literal['buy', 'sell'], amount: Literal[1, 2], *, item: str):
    await ctx.send(f'{buy_sell.capitalize()}ing {amount} {item}(s)!')

buy_sell パラメータはリテラル文字列の "buy" または "sell" のどちらかで、 amountint1 または 2 に変換されなければなりません。 buy_sell または amount がどの値にも一致しない場合は、特別なエラー BadLiteralArgument が発生します。任意のリテラル値は、同じ typing.Literal コンバーター内で混合してマッチさせることができます。

typing.Literal[True]typing.Literal[False]bool コンバーターのルールに従っていることに注意してください。

typing.Annotated

バージョン 2.0 で追加.

typing.Annotated は Python 3.9 で導入された特別な型です。 これを使用すると、型チェッカは1つのタイプを参照できますが、ライブラリは別のタイプを参照できます。 これは複雑なコンバータにて型チェッカを通すのに役立ちます。 Annotated の2番目のパラメータはライブラリが使用するコンバータでなければなりません。

例えば、以下の例では:

from typing import Annotated

@bot.command()
async def fun(ctx, arg: Annotated[str, lambda s: s.upper()]):
    await ctx.send(arg)

型チェッカは arg を通常の str として認識しますが、ライブラリは入力をすべて大文字に変更したいことを知っています。

注釈

Python 3.9未満では、 typing_extensions ライブラリをインストールして Annotated をそこからインポートすることを推奨します。

Greedy

Greedy コンバーターは引数にリストが適用される以外は typing.Optional を一般化したものです。簡単に言うと、与えられた引数を変換ができなくなるまで指定の型に変換しようと試みます。

次の例をみてください:

@bot.command()
async def slap(ctx, members: commands.Greedy[discord.Member], *, reason='no reason'):
    slapped = ", ".join(x.name for x in members)
    await ctx.send(f'{slapped} just got slapped for {reason}')

これが呼び出されると、任意の数のメンバーを渡すことができます:

../../_images/greedy1.png

このコンバータを利用した際に渡される型は、その対象となっているパラメータの種類によって異なります。

  • 位置パラメータの場合、型はデフォルトのものか変換された値からなる list になります。

  • 可変長パラメータの場合、型は通常同様 tuple になります。

  • キーワードパラメータの場合、型は Greedy を使用していないときと同じになります。

Greedy パラメータはデフォルト値を指定することでオプションにすることもできます。

typing.Optional コンバータと併用することで、シンプルかつ表現に富む呼び出し構文を提供できます。

import typing

@bot.command()
async def ban(ctx, members: commands.Greedy[discord.Member],
                   delete_days: typing.Optional[int] = 0, *,
                   reason: str):
    """Mass bans members with an optional delete_days parameter"""
    delete_seconds = delete_days * 86400 # one day
    for member in members:
        await member.ban(delete_message_seconds=delete_seconds, reason=reason)

このコマンドは以下のような方法で呼び出すことが可能です。

$ban @Member @Member2 spam bot
$ban @Member @Member2 7 spam bot
$ban @Member spam

警告

Greedytyping.Optional の利用は強力かつ便利である反面、その代償として一部の人が驚いてしまうような曖昧な構文解析を許容することとなります。

例えば、 discord.Membertyping.Optional の後に int が続くようなシグネチャでは MemberConverter がメンバー取得のために様々な方法をとることが要因となり、名前が数字になっているメンバーを取得してしまう可能性があります。コードが意図しない曖昧な構文解析を引き起こさないよう注意してください。テクニックの一つとして、カスタムコンバーターを用いて予期される構文の許容を制限するか、このような衝突を最小限に抑えるために、パラメータを並び替えることなどが挙げられます。

曖昧な構文解析を防ぐため、 strNonetyping.Optional 、そして GreedyGreedy コンバーターのパラメーターにするのは禁止されています。

discord.Attachment

バージョン 2.0 で追加.

discord.Attachment コンバータはメッセージにアップロードされた添付ファイルから添付ファイルを一個取得する特別なコンバータです。このコンバータは、メッセージ内容は 確認せず アップロードされた添付ファイルのみ確認します。

次の例をみてください:

import discord

@bot.command()
async def upload(ctx, attachment: discord.Attachment):
    await ctx.send(f'You have uploaded <{attachment.url}>')

コマンドの呼び出し時に、ユーザーはコマンドを実行するときにはファイルを直接アップロードしないといけません。これを typing.Optional コンバータと組み合わせた場合、添付ファイルを提供する必要はありません。

import typing
import discord

@bot.command()
async def upload(ctx, attachment: typing.Optional[discord.Attachment]):
    if attachment is None:
        await ctx.send('You did not upload anything!')
    else:
        await ctx.send(f'You have uploaded <{attachment.url}>')

これは複数の添付ファイルでも動作します:

import typing
import discord

@bot.command()
async def upload_many(
    ctx,
    first: discord.Attachment,
    second: typing.Optional[discord.Attachment],
):
    if second is None:
        files = [first.url]
    else:
        files = [first.url, second.url]

    await ctx.send(f'You uploaded: {" ".join(files)}')

この例では、ユーザーは少なくとも1つのファイルを提供する必要がありますが、2つ目のファイルはオプションです。

特別なケースとして、 Greedy を使用すると、存在する場合はメッセージ内の残りの添付ファイルが返されます。

import discord
from discord.ext import commands

@bot.command()
async def upload_many(
    ctx,
    first: discord.Attachment,
    remaining: commands.Greedy[discord.Attachment],
):
    files = [first.url]
    files.extend(a.url for a in remaining)
    await ctx.send(f'You uploaded: {" ".join(files)}')

なお、 discord.AttachmentGreedy の後に discord.Attachment コンバータを使用するのは、Greedyが残りの添付ファイルをすでに使用しているため、常に失敗します。

添付ファイルが期待されているのに与えられていない場合、 MissingRequiredAttachment がエラーハンドラに送出されます。

FlagConverter

バージョン 2.0 で追加.

FlagConverter を使用すると、型アノテーション PEP 526 を使用してユーザーフレンドリーな「フラグ」を指定したり、 dataclasses モジュールを彷彿とさせる構文を使用できます。

例えば、以下のコードです。

from discord.ext import commands
import discord

class BanFlags(commands.FlagConverter):
    member: discord.Member
    reason: str
    days: int = 1

@commands.command()
async def ban(ctx, *, flags: BanFlags):
    plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day'
    await ctx.send(f'Banned {flags.member} for {flags.reason!r} (deleted {plural} worth of messages)')

フラグに似たシンプルな構文を使用してコマンドを呼び出すことができます:

../../_images/flags1.png

フラグは、フラグに値を渡す際に引用符を必要としない構文を使用しています。フラグ構文の目標は、できるだけユーザーフレンドリーにすることです。このため、複数のノブを使用する複雑なコマンドや、外部コマンド・インターフェースでキーワードのみのパラメータをシミュレートする場合、フラグを使用するのが適しています。 フラグ・コンバータでは、キーワード専用パラメータを使用することをお勧めします。 これにより、適切な解析と引用符での動作が保証されます。

内部的には、FlagConverter クラスがクラスを調べてフラグを見つけます。フラグには、型アノテーションが付いたクラス変数と、 flag() 関数の結果が代入されたクラス変数があります。これらのフラグは、ユーザーが使用するインターフェースを定義するために使用されます。アノテーションは、フラグの引数が準拠しなければならないコンバーターに対応しています。

ほとんどの場合、フラグを定義するために余分な作業は必要ありません。 しかし、フラグ名やデフォルト値を制御するためにカスタマイズが必要な場合は、 flag() 関数が便利です:

from typing import List

class BanFlags(commands.FlagConverter):
    members: List[discord.Member] = commands.flag(name='member', default=lambda ctx: [])

これは members 属性が member というフラグにマップされ、デフォルト値が空のリストであることをパーサーに伝えます。 カスタマイズ性を向上させるために、デフォルトは値か呼び出し可能な値で、 Context を唯一のパラメータとして取ります。この呼び出し可能な値は関数またはコルーチンのいずれかを使用できます。

A positional flag can be defined by setting the positional attribute to True. This tells the parser that the content provided before the parsing occurs is part of the flag. This is useful for commands that require a parameter to be used first and the flags are optional, such as the following:

class BanFlags(commands.FlagConverter):
    members: List[discord.Member] = commands.flag(name='member', positional=True, default=lambda ctx: [])
    reason: Optional[str] = None

注釈

Only one positional flag is allowed in a flag converter.

フラグ構文をカスタマイズするために、クラスのパラメーターリストに渡せるオプションもいくつか用意されています。

# --hello world syntax
class PosixLikeFlags(commands.FlagConverter, delimiter=' ', prefix='--'):
    hello: str


# /make food
class WindowsLikeFlags(commands.FlagConverter, prefix='/', delimiter=''):
    make: str

# TOPIC: not allowed nsfw: yes Slowmode: 100
class Settings(commands.FlagConverter, case_insensitive=True):
    topic: Optional[str]
    nsfw: Optional[bool]
    slowmode: Optional[int]

# Hello there --bold True
class Greeting(commands.FlagConverter):
    text: str = commands.flag(positional=True)
    bold: bool = False

注釈

Despite the similarities in these examples to command like arguments, the syntax and parser is not a command line parser. The syntax is mainly inspired by Discord's search bar input and as a result all flags need a corresponding value unless part of a positional flag.

フラグコンバータは FlagError 派生の例外のみ送出します。フラグ変換中にエラーが発生した場合、 BadFlagArgument が代わりに送出され、元の例外は original 属性でアクセスできます。

フラグコンバーターは通常のコマンドと似ており、ほとんどのタイプのコンバータを型アノテーションとして使用できます(例外は Greedy) 。以下で説明するように、特定のアノテーションに対する追加のサポートが追加されます。

typing.List

リストがフラグアノテーションとして与えられた場合、引数が何回も渡せることをパーサーに知らせます。

例えば、上記の例を拡張すると:

from discord.ext import commands
from typing import List
import discord

class BanFlags(commands.FlagConverter):
    members: List[discord.Member] = commands.flag(name='member')
    reason: str
    days: int = 1

@commands.command()
async def ban(ctx, *, flags: BanFlags):
    for member in flags.members:
        await member.ban(reason=flags.reason, delete_message_days=flags.days)

    members = ', '.join(str(member) for member in flags.members)
    plural = f'{flags.days} days' if flags.days != 1 else f'{flags.days} day'
    await ctx.send(f'Banned {members} for {flags.reason!r} (deleted {plural} worth of messages)')

これはフラグを繰り返し指定することで呼び出されます:

../../_images/flags2.png

typing.Tuple

フラグを何度も指定する場合、上記の構文は少し繰り返しになるので、 tuple 型アノテーションを使用すると、可変タプルを使用した「欲張りな」セマンティクスを実現することができます。

from discord.ext import commands
from typing import Tuple
import discord

class BanFlags(commands.FlagConverter):
    members: Tuple[discord.Member, ...]
    reason: str
    days: int = 1

これにより、以前の ban コマンドを以下のように呼び出すことができます。

../../_images/flags3.png

tuple アノテーションはペアの解析を可能にします。例えば、以下のコードがあります。

# point: 10 11 point: 12 13
class Coordinates(commands.FlagConverter):
    point: Tuple[int, int]

警告

解析が曖昧になってしまうため、パーサーはタプル引数がスペースを必要とする場合、引用符で囲むことを期待します。そのため、もし内部型のひとつが str で、その引数がスペースを必要とする場合には、タプルの他の要素と区別するために引用符が使用されなければなりません。

typing.Dict

dict アノテーションは、list ではなく、dict として与えられた戻り値の値を除いて、List[Tuple[K, V]] と同等です

ハイブリッドコマンドでの動作

ハイブリッドコマンドとして使用された場合、パラメータはアプリケーションコマンドでは別々のものになります。例えば、次のコンバータは:

class BanFlags(commands.FlagConverter):
    member: discord.Member
    reason: str
    days: int = 1


@commands.hybrid_command()
async def ban(ctx, *, flags: BanFlags):
    ...

以下のように定義されたアプリケーションコマンドと同等です:

@commands.hybrid_command()
async def ban(ctx, member: discord.Member, reason: str, days: int = 1):
    ...

これは、パラメータを名前で参照するデコレータはフラグ名を代わりに使用するということです。

class BanFlags(commands.FlagConverter):
    member: discord.Member
    reason: str
    days: int = 1


@commands.hybrid_command()
@app_commands.describe(
    member='The member to ban',
    reason='The reason for the ban',
    days='The number of days worth of messages to delete',
)
async def ban(ctx, *, flags: BanFlags):
    ...

使いやすいように、 flag() 関数は description キーワード引数を受け取るので、説明をインラインで渡すことができます。

class BanFlags(commands.FlagConverter):
    member: discord.Member = commands.flag(description='The member to ban')
    reason: str = commands.flag(description='The reason for the ban')
    days: int = commands.flag(default=1, description='The number of days worth of messages to delete')


@commands.hybrid_command()
async def ban(ctx, *, flags: BanFlags):
    ...

同様に、 name キーワード引数を使用すると、 rename() デコレータと同様に、パラメータの改名ができます。

ハイブリッドコマンド形式では、Discordの制限によりいくつかのアノテーションがサポートされていないことに注意してください。

  • typing.Tuple

  • typing.List

  • typing.Dict

注釈

ハイブリッドコマンド1個ごとにフラグコンバータ1個までサポートされています。 フラグコンバーターの動作の仕組みのため、あるシグネチャに2個フラグコンバータが存在するのはまれです。

パラメータのメタデータ

parameter() はカスタムメタデータを Command のパラメータに割り当てます。

これは以下の場合に便利です:

  • パラメータのアノテーションにカスタムコンバーターを付するカスタムコンバーターはランタイムで動作するため、型チェッカが理解できず、問題が発生します。

    class SomeType:
        foo: int
    
    class MyVeryCoolConverter(commands.Converter[SomeType]):
        ...  # implementation left as an exercise for the reader
    
    @bot.command()
    async def bar(ctx, cool_value: MyVeryCoolConverter):
        cool_value.foo  # type checker warns MyVeryCoolConverter has no value foo (uh-oh)
    

    しかし、 parameter() を使用すれば、型チェッカが何が起こっているかを理解できるようになります。

    @bot.command()
    async def bar(ctx, cool_value: SomeType = commands.parameter(converter=MyVeryCoolConverter)):
        cool_value.foo  # no error (hurray)
    
  • 既定値の動的評価

    @bot.command()
    async def wave(ctx, to: discord.User = commands.parameter(default=lambda ctx: ctx.author)):
        await ctx.send(f'Hello {to.mention} :wave:')
    

    これが非常に一般的な使用法であることから、ライブラリは AuthorCurrentChannelCurrentGuild を提供します。これらを使用すると wave は以下のように簡素化できます:

    @bot.command()
    async def wave(ctx, to: discord.User = commands.Author):
        await ctx.send(f'Hello {to.mention} :wave:')
    

    Author などは、表示される既定値が事前に指定されているなど、他の利点もあります。

エラーハンドリング

コマンドの解析に失敗すると、デフォルトではエラーの発生とそれが握り潰されたことを知らせるノイズのようなエラーがコンソールの stderr に出力されます。

エラーを処理するには、エラーハンドラと呼ばれるものを利用する必要があります。 on_command_error() グローバルエラーハンドラが存在し、これは イベントリファレンス のイベントのように動作します。 このハンドラはエラーが発生するたびに呼び出されます。

しかし、ほとんどの場合においては、コマンド自体に対応するローカルなエラー処理を行いたいと考えるでしょう。幸いなことに、コマンドにはローカルエラーハンドラが存在するため、これを利用して実現することができます。まず、エラーハンドラとして利用する関数を error() でデコレートします。

@bot.command()
async def info(ctx, *, member: discord.Member):
    """Tells you some info about the member."""
    msg = f'{member} joined on {member.joined_at} and has {len(member.roles)} roles.'
    await ctx.send(msg)

@info.error
async def info_error(ctx, error):
    if isinstance(error, commands.BadArgument):
        await ctx.send('I could not find that member...')

ハンドラの最初の引数には Context が渡され、2番目の引数には CommandError が渡されます。 エラー一覧は 例外 から見ることができます。

チェック

コマンドをユーザーに使ってほしくない場合などがあります。例えば、使用者が権限を持っていない場合や、ボットによりブロックされている場合などです。コマンド拡張機能ではこのような機能を Checks と呼び、完全にサポートしています。

チェックは Context を引数とする関数です。関数はこれらの選択ができます:

  • True を返し、その人がコマンドを実行できることを示します。

  • False を返し、その人がコマンドを実行できないことを示します。

  • CommandError を継承する例外を発生させ、コマンドを実行できないことを示します。

チェックを登録するには2つの方法があります:1つ目は check() を使う方法です。

async def is_owner(ctx):
    return ctx.author.id == 316026178463072268

@bot.command(name='eval')
@commands.check(is_owner)
async def _eval(ctx, *, code):
    """A bad example of an eval command"""
    await ctx.send(eval(code))

例えば、この場合は is_ownerTrue だったときのみコマンドを実行します。しかし、チェックを使い回すために独自のデコレーターにしたくなることもあるでしょう。そうしたい場合は、

def is_owner():
    async def predicate(ctx):
        return ctx.author.id == 316026178463072268
    return commands.check(predicate)

@bot.command(name='eval')
@is_owner()
async def _eval(ctx, *, code):
    """A bad example of an eval command"""
    await ctx.send(eval(code))

このようにすると独自のデコレーターになります。 このチェックはとてもよく使われるため、ライブラリに標準で実装されています( is_owner() )。

@bot.command(name='eval')
@commands.is_owner()
async def _eval(ctx, *, code):
    """A bad example of an eval command"""
    await ctx.send(eval(code))

複数のチェックが渡されたときには、 すべて のチェックが True になる必要があります。

def is_in_guild(guild_id):
    async def predicate(ctx):
        return ctx.guild and ctx.guild.id == guild_id
    return commands.check(predicate)

@bot.command()
@commands.is_owner()
@is_in_guild(41771983423143937)
async def secretguilddata(ctx):
    """super secret stuff"""
    await ctx.send('secret stuff')

もしチェックのうちどれかが失敗した場合、コマンドは実行されません。

もし例外が発生した場合、 エラーハンドラ によって例外が処理されます。もし CommandError を継承しないエラーを発生させた場合、 CheckFailure が発生します。

@bot.command()
@commands.is_owner()
@is_in_guild(41771983423143937)
async def secretguilddata(ctx):
    """super secret stuff"""
    await ctx.send('secret stuff')

@secretguilddata.error
async def secretguilddata_error(ctx, error):
    if isinstance(error, commands.CheckFailure):
        await ctx.send('nothing to see here comrade.')

もし強化されたエラーシステムが必要な場合は、例外を継承し、False を返す代わりに例外を発生させることができます。

class NoPrivateMessages(commands.CheckFailure):
    pass

def guild_only():
    async def predicate(ctx):
        if ctx.guild is None:
            raise NoPrivateMessages('Hey no DMs!')
        return True
    return commands.check(predicate)

@bot.command()
@guild_only()
async def test(ctx):
    await ctx.send('Hey this is not a DM! Nice.')

@test.error
async def test_error(ctx, error):
    if isinstance(error, NoPrivateMessages):
        await ctx.send(error)

注釈

guild_only デコレータはよく使われるため、標準で実装されています( guild_only() )。

グローバルチェック

すべての コマンドにチェックをかけたいこともあるでしょう。そうしたい場合は、ライブラリのグローバルチェックを使うことができます。

グローバルチェックは、 Bot.check() デコレータで登録されることを除き、通常のチェックと同様に動作します。

例えば、全DMをブロックするには、次の操作を行います。

@bot.check
async def globally_block_dms(ctx):
    return ctx.guild is not None

警告

グローバルチェックを追加するときには注意して下さい。ボットを操作できなくなる可能性があります。

ハイブリッドコマンド

バージョン 2.0 で追加.

commands.HybridCommand は、テキストコマンドとしても、スラッシュコマンドとしても呼び出せるコマンドです。これを使用すれば、別々のコードを書かずにコマンドをスラッシュコマンドとテキストコマンドの両方として定義できます。

ハイブリッドコマンドを定義するには、コマンドコールバックを Bot.hybrid_command() デコレータで装飾しないといけません。

@bot.hybrid_command()
async def test(ctx):
    await ctx.send("This is a hybrid command!")

上のコマンドはテキストコマンドとスラッシュコマンドの両方として実行できます。なお、スラッシュコマンドを表示するには、 sync を呼び出して CommandTree を手動で同期しないといけません。

../../_images/hybrid1.png ../../_images/hybrid2.png

Bot.hybrid_group() デコレータを使用して、ハイブリッドコマンドグループとサブコマンドを作成できます。

@bot.hybrid_group(fallback="get")
async def tag(ctx, name):
    await ctx.send(f"Showing tag: {name}")

@tag.command()
async def create(ctx, name):
    await ctx.send(f"Created tag: {name}")

Discordの制限により、 スラッシュコマンドグループは直接呼び出すことができないため、 fallback パラメータを使用して、親グループのコールバックを呼び出すサブコマンドを作成できます。

../../_images/hybrid3.png ../../_images/hybrid4.png

スラッシュコマンドには制限があるため、ハイブリッドコマンドではテキストコマンドの一部の機能がサポートされていません。スラッシュコマンドでサポートされている機能のみ使用している場合にハイブリッドコマンドを定義できます。

以下は現時点でハイブリッドコマンドではサポート されていません:

  • 可変長引数。例: *arg: int

  • 深さが1より大きいグループコマンド。

  • ほとんどの typing.Union 型。
    • チャンネルの型のユニオン型は使用できます

    • ユーザーの型のユニオン型は使用できます

    • チャンネルの型とロールの型のユニオン型は使用できます

それ以外の、コンバーター、チェック、オートコンプリート、フラグ、その他はすべてハイブリッドコマンドで利用できます。なお、設計上の制限により、 discord.app_commands.autocomplete() といったアプリケーションコマンド関連のデコレータは hybrid_command() デコレータの下に配置しないといけません。

コードを簡単に書くために、 Context クラスのメソッドや属性の動作が変化します:

  • Context.interaction を用いてスラッシュコマンドのインタラクションを取得できます。

  • インタラクションは一度しか応答できないため、 Context.send() は、インタラクション応答とフォローアップ応答のどちらを送信するかを自動的に決定します。

  • Context.defer() はスラッシュコマンドではインタラクション応答を遅らせ、テキストコマンドでは入力インジケーターを表示します。