コマンドの登録¶
Discord がコマンドを表示するのは、アプリがそのコマンドを登録した後だけです。bostie はこれを代わりに行います。Bot の起動時に、定義したコマンドと Discord が保持しているコマンドを比較し、違いがある場合にだけ送信します。
起動時の同期¶
bot.start() と bot.run() は、ゲートウェイに接続する前に同期を行います。bostie はスコープ(グローバル、または 1 つのサーバー)ごとに、登録済みのコマンドをローカライズ付きで取得してフィールドごとに比較し、違いがあればそのスコープのコマンドを 1 回のリクエストで置き換えます。ログには commands up to date (global) または commands synced (global): added [...], removed [...], changed [...] と出力されます。
Discord が保持していても、もう定義していないコマンドは削除されます。ただし、一度も定義しない種類のコマンドは例外です。@bot.entry_point のない Bot では、Discord がアクティビティのために作ったエントリーポイントコマンドが残ります。
Bot(auto_sync=False) を指定すると同期を行いません。await bot.sync_commands() は、変更の有無にかかわらずすべてのスコープのコマンドをその場で置き換え、スコープごとに SyncResult を返します。これには added、removed、changed になった名前と、Discord がスラッシュコマンドに割り当てた ids が入っています。
スコープ¶
guilds のないコマンドはグローバルです。アプリをインストールしたすべてのサーバーで動作し、contexts が許していれば DM でも動作します。guilds=[...] を指定すると、そのサーバーにだけ登録されます。
Bot の開発中は、すべてをテスト用のサーバーに登録してください。
import os
import bostie
from bostie import Context, Permissions
dev_guild = int(os.environ.get("DEV_GUILD", "0"))
bot = bostie.Bot(dev_guilds=[dev_guild] if dev_guild else [])
@bot.command
async def about(ctx: Context) -> None:
"""Explain how to use the bot."""
sync = await bot.command_mention("sync")
await ctx.respond(f"Admins can run {sync} after an update.")
@bot.command(default_permissions=Permissions.ADMINISTRATOR, ephemeral=True)
async def sync(ctx: Context) -> None:
"""Send the commands to Discord again."""
results = await bot.sync_commands()
changed = sum(len(r.added) + len(r.removed) + len(r.changed) for r in results)
await ctx.respond(f"{changed} commands changed.")
Bot(dev_guilds=[...]) は、自身の guilds を持たないすべてのコマンドを、グローバルではなくそのサーバーに登録します。エントリーポイントコマンドは常にグローバルなので、同期はこれを除外して警告を出します。
複数のスコープで同じ名前を使う¶
import bostie
from bostie import Context
bot = bostie.Bot()
EVENTS_GUILD = 1335172792320131073
@bot.command(name="rules")
async def rules(ctx: Context) -> None:
"""Show the rules of the bot."""
await ctx.respond("Be kind and stay on topic.")
@bot.command(name="rules", guilds=[EVENTS_GUILD])
async def event_rules(ctx: Context) -> None:
"""Show the rules of this server's events."""
await ctx.respond("Sign up before the event starts.")
同じサーバーに 2 つ存在しない限り、コマンドは名前を共有できます。たとえば、グローバルの /rules と 1 つのサーバー用の /rules、あるいはサーバーごとの /rules です。それぞれが自分のスコープに登録され、インタラクションはそのサーバーに登録されたコマンドを実行し、なければグローバルのコマンドを実行します。そのサーバーでは、Discord はグローバルのコマンドとサーバーのコマンドの両方を一覧に表示します。dev_guilds の下では、サーバー独自のコマンドがそのサーバーでのグローバルのコマンドの代わりになります。同じ名前のコマンドが 2 つあり、そのサーバーが重なっている場合や、どちらもグローバルの場合は、2 つ目を登録した時点で CommandDefinitionError が送出されます。ユーザーコマンドとメッセージコマンドも同じ規則に従います。
同期が扱うのは、コマンドがあるスコープだけです。dev_guilds をやめても、コマンドはテスト用のサーバーに新しいグローバルのコマンドと並んで残ります。await bot.rest.commands.bulk_overwrite_guild(bot.application_id, guild_id, []) で削除できます。
コマンドを使える場所¶
contexts と integration_types はグローバルコマンドに適用されます。コマンド、ルーター、Bot のいずれかに設定します。
contexts:InteractionContextType.GUILD、BOT_DM(Bot との DM)、PRIVATE_CHANNEL(ユーザーインストールされたアプリでの、その他の DM とグループ DM)。integration_types:ApplicationIntegrationType.GUILD_INSTALLとUSER_INSTALL。
Bot(contexts=[...], integration_types=[...]) は、自分で設定していないすべてのグローバルコマンドの既定値になります。どこにも設定しなければ、bostie はどちらのフィールドも送らず、Discord の既定値が適用されます。integration_types の場合、それはアプリに設定されたインストールコンテキストです。設定していた値を外すことも変更とみなされるので、次の同期でコマンドは再び Discord の既定値に戻ります。
コマンドのメンション¶
await bot.command_mention("roles color set") は </roles color set:ID> を返し、Discord はこれをクリックできるコマンドとして表示します。直前の同期で得た ID を使うか、スコープごとに 1 回 Discord に問い合わせます。guild_id= を指定すると、そのサーバーに登録されたコマンドが同じ名前のグローバルのコマンドより優先されます。未知の名前を渡すと LookupError が送出され、Bot の起動前に呼び出すと StateError が送出されます。すでに ID を持っている場合は、fmt.command(name, command_id) で同じテキストを組み立てられます。
翻訳¶
コマンド、グループ、サブコマンド、Option、Choice の name_localizations と description_localizations は、1 つのテキストを翻訳します。翻訳関数を使うと、すべてをまとめて翻訳できます。
import bostie
from bostie import Context, Locale
TRANSLATIONS = {
Locale.FRENCH: {"greet": "saluer", "Greet someone": "Saluer quelqu'un"},
Locale.GERMAN: {"greet": "grüßen", "Greet someone": "Jemanden grüßen"},
}
def translate(text: str, locale: Locale) -> str | None:
return TRANSLATIONS.get(locale, {}).get(text)
bot = bostie.Bot(translate=translate)
@bot.command
async def greet(ctx: Context) -> None:
"""Greet someone."""
await ctx.respond(f"Hello! Your client is set to {ctx.locale.value}.")
Bot(translate=function) は同期の際に、すべてのコマンド、グループ、サブコマンド、オプションのすべての名前と説明、そしてすべての選択肢の名前について、Locale ごとに 1 回ずつ function(text, locale) を呼び出します。None を返すか、テキストを変更せずに返すと、そのロケールには何も追加されません。自分で書いたローカライズは関数の結果より優先されます。ハンドラーでは、ユーザーの言語は ctx.locale、サーバーの言語は ctx.interaction.guild_locale で確認できます。
Discord がコマンドを拒否したとき¶
Discord が 400 で拒否した同期では、CommandSyncError が送出されます。そのメッセージには問題ごとに 1 行ずつ、コマンド、オプションのパス、Discord のメッセージが示され、error.problems にはそれらが SyncProblem オブジェクトとして入っています。ほとんどの問題はもっと早い段階で見つかります。デコレーターが実行時に名前、説明、上限をチェックするためです。