Слэш-команды¶

Слэш-команда — это асинхронная функция с декоратором @bot.command. Её первый параметр принимает Context; каждый следующий параметр — это параметр команды.
from typing import Annotated
import bostie
from bostie import Attachment, Context, PartialChannel, ResolvedMember, Role
bot = bostie.Bot()
@bot.command
async def report(
ctx: Context,
member: Annotated[ResolvedMember, "Who to report"],
reason: Annotated[str, "What happened"],
channel: PartialChannel | None = None,
proof: Attachment | None = None,
notify: Role | None = None,
urgent: bool = False,
) -> None:
"""Report a member to the moderators."""
lines = [f"{member.mention}: {reason}"]
if channel is not None:
lines.append(f"In {channel.mention}")
if proof is not None:
lines.append(f"Proof: {proof.url}")
if notify is not None:
lines.append(f"Notify {notify.mention}")
if urgent:
lines.append("Urgent")
await ctx.respond("\n".join(lines), ephemeral=True)
Команда называется /report. Её имя — это имя функции, а описание — первая строка докстринга; @bot.command(name=..., description=...) задаёт их явно, а команда без того и другого получает описание …. Работают и @bot.command, и @bot.command(...) — как на боте, так и на роутере.
Типы параметров¶
Аннотация типа параметра выбирает тип параметра команды, а обработчик получает уже преобразованное значение:
| Аннотация | Параметр Discord | Обработчик получает |
|---|---|---|
str |
строка | текст |
int |
целое число | int |
float |
число | float, даже если пользователь ввёл целое число |
bool |
логическое значение | bool |
User |
пользователь | User |
ResolvedMember |
пользователь | участника вместе с его user; только на серверах |
Role |
роль | Role |
PartialChannel |
канал | канал в том виде, в каком его разрешило взаимодействие |
Attachment |
вложение | Attachment загруженного файла |
User \| Role или ResolvedMember \| Role |
упоминаемый объект | то, что выбрал пользователь |
Literal[...] или Enum |
строка, целое число или число с вариантами | литерал или член перечисления |
Аннотация, которой нет в таблице, вызывает CommandDefinitionError при выполнении декоратора. Для параметра-канала аннотация — PartialChannel, а не Channel, а для участника — ResolvedMember, а не Member: взаимодействие несёт именно эти частичные объекты, о чём и говорит сообщение об ошибке.
Обязательные и необязательные¶
Параметр без значения по умолчанию обязателен. Значение по умолчанию или | None в аннотации делают его необязательным; необязательный параметр, который пользователь не указал, получает значение по умолчанию или None. Discord требует, чтобы обязательные параметры шли первыми, и bostie сам сортирует их так при сборке команды, в каком бы порядке ни шли параметры функции.
Описания¶
Строка в Annotated — это описание параметра: Annotated[str, "What happened"]. Вместо этого описания можно держать в докстринге:
import bostie
from bostie import Context, Range
bot = bostie.Bot()
@bot.command
async def remind(ctx: Context, minutes: Range[int, 1, 1440], text: str) -> None:
"""Set a reminder.
Args:
minutes: How long to wait, up to a day.
text: What to remind you of.
"""
await ctx.respond(f"In {minutes} minutes: {text}", ephemeral=True)
bostie читает раздел Args:, как выше, и раздел Parameters, подчёркнутый дефисами; описание может занимать несколько строк. Строка в Annotated важнее Option(description=), а тот важнее докстринга. Параметр, который нигде не описан, получает ….
Диапазоны и настройки параметров¶
Range[int, 1, 10] — параметр int от 1 до 10 включительно. Range принимает и float, а для str границы задают длину: Range[str, 1, 2000]. Всё остальное задаёт Option(...) в Annotated, и он важнее того, что следует из аннотации:
from typing import Annotated
import bostie
from bostie import Attachment, ChannelType, Context, Option, PartialChannel, Range
bot = bostie.Bot()
@bot.command
async def announce(
ctx: Context,
text: Range[str, 1, 2000],
target: Annotated[
PartialChannel,
Option(
name="channel",
description="Where to post it",
channel_types=[ChannelType.GUILD_TEXT, ChannelType.GUILD_ANNOUNCEMENT],
description_localizations={"de": "Wo es erscheinen soll"},
),
],
delay: Annotated[float, Option(min=0, max=3600, description="Seconds to wait")] = 0,
image: Annotated[Attachment | None, Option(file_types=["image"])] = None,
) -> None:
"""Post an announcement."""
extra = f" with {image.filename}" if image is not None else ""
await ctx.respond(f"Posting in {target.mention} in {delay:g} s{extra}: {text}")
Поле Option |
Применяется к | Значение |
|---|---|---|
name |
всем | имя параметра в Discord; параметр функции сохраняет своё имя в Python |
description |
всем | описание, если в Annotated нет строки |
name_localizations, description_localizations |
всем | текст для каждой локали, например {"fr": "salon"} |
min, max |
целому числу, числу | границы включительно, в пределах ±2⁵³ |
min_length, max_length |
строке | границы длины: от 0 до 6000 и от 1 до 6000 |
channel_types |
каналу | типы каналов, которые может выбрать пользователь |
file_types |
вложению | до 10 значений из image, video, audio или расширений вроде .pdf |
choices |
строке, целому числу, числу | фиксированные варианты; см. ниже |
autocomplete |
строке, целому числу, числу | функция, которая предлагает значения, пока пользователь печатает |
extra |
всем | сырые поля, которые добавляются в JSON параметра, — для поля, которое bostie ещё не моделирует |
Поле, указанное для типа параметра, к которому оно не применяется, вызывает CommandDefinitionError.
Варианты¶
Literal или Enum предлагают фиксированный список. Вариант из Literal называется по своему значению; вариант из Enum называется по имени члена и отправляет значение члена. Option(choices=[...]) переименовывает или локализует их, если перечисляет те же значения в том же порядке:
from enum import Enum
from typing import Annotated, Literal
import bostie
from bostie import Choice, Context, Option
bot = bostie.Bot()
class Size(Enum):
SMALL = 1
MEDIUM = 2
LARGE = 3
@bot.command
async def order(
ctx: Context,
drink: Literal["tea", "coffee", "water"],
size: Size = Size.MEDIUM,
milk: Annotated[
Literal["none", "oat", "cow"],
Option(
choices=[
Choice("No milk", "none"),
Choice("Oat milk", "oat", name_localizations={"fr": "Lait d'avoine"}),
Choice("Cow milk", "cow"),
]
),
] = "none",
) -> None:
"""Order a drink."""
await ctx.respond(f"One {size.name.lower()} {drink}, milk: {milk}.")
Обработчик /order получает "tea", "coffee" или "water" для drink, член Size для size и значение выбранного Choice для milk. При простой аннотации str, int или float варианты задаёт один только Option(choices=[...]); каждый из них — это Choice(name, value) или просто значение, которое называется само по себе. Значения вариантов должны иметь тип параметра, а значения Literal или Enum должны быть все str, все int или все float. У параметра не больше 25 вариантов, имя варианта — от 1 до 100 символов, а строковое значение — не длиннее 100.
Автодополнение¶
Option(autocomplete=function) запрашивает у function подсказки, пока пользователь печатает. Функция получает AutocompleteContext и уже введённый текст и возвращает значения или объекты Choice:
from typing import Annotated
import bostie
from bostie import AutocompleteContext, Choice, Context, Option
bot = bostie.Bot()
CITIES = {
"fr": ["Paris", "Lyon", "Marseille"],
"jp": ["Tokyo", "Osaka", "Kyoto"],
}
async def countries(ctx: AutocompleteContext, typed: str) -> list[Choice]:
return [
Choice(code.upper(), code) for code in CITIES if code.startswith(typed.lower())
]
async def cities(ctx: AutocompleteContext, typed: str) -> list[str]:
country = ctx.options.get("country", "")
return [city for city in CITIES.get(country, []) if typed.lower() in city.lower()]
@bot.command
async def weather(
ctx: Context,
country: Annotated[str, Option(autocomplete=countries)],
city: Annotated[str, Option(autocomplete=cities)],
) -> None:
"""Show the weather of a city."""
await ctx.respond(f"Sunny in {city}, {country.upper()}.")
ctx.options хранит уже заполненные параметры по их именам, преобразованные так, как их получил бы обработчик, если преобразование удаётся; параметр, который сейчас вводится, остаётся текстом. bostie отправляет первые 25 подсказок, обрезает имена длиннее 100 символов и отбрасывает подсказки с пустым именем, значением неверного типа или строковым значением длиннее 100 символов, с предупреждением в каждом случае. У параметра не может быть одновременно choices и autocomplete. Если функция выбрасывает исключение, ошибка уходит в хуки ошибок, а Discord получает пустой список.
Конвертеры¶
Annotated[T, Convert(function)] превращает значение параметра в T до запуска обработчика. function(ctx, value) может быть асинхронной, а аннотация типа её параметра value определяет тип параметра в Discord, включая Range и описания из Annotated:
from dataclasses import dataclass
from typing import Annotated
import bostie
from bostie import Context, Convert, Range
bot = bostie.Bot()
@dataclass(frozen=True)
class Color:
red: int
green: int
blue: int
def parse_color(ctx: Context, text: Range[str, 7, 7]) -> Color:
value = int(text.removeprefix("#"), 16)
return Color(value >> 16, value >> 8 & 0xFF, value & 0xFF)
@bot.command
async def paint(
ctx: Context,
color: Annotated[Color, Convert(parse_color), "A hex color such as #5865F2"],
) -> None:
"""Paint with a color."""
await ctx.respond(f"Red {color.red}, green {color.green}, blue {color.blue}.")
Конвертер выполняется только для параметра, который указал пользователь; необязательный параметр, который не указали, получает значение по умолчанию без преобразования. Исключение из конвертера уходит в хуки ошибок так же, как исключение из обработчика.
Подкоманды и группы¶
bot.group(name, description) регистрирует команду, состоящую из подкоманд, а group.group(...) добавляет внутрь неё группу подкоманд. Группы вкладываются только на один уровень, и в группе не больше 25 элементов:
import bostie
from bostie import Context, Permissions, ResolvedMember, Role
bot = bostie.Bot()
roles = bot.group("roles", "Manage roles", default_permissions=Permissions.MANAGE_ROLES)
colors = roles.group("color", "Color roles")
@roles.command
async def give(ctx: Context, member: ResolvedMember, role: Role) -> None:
"""Give a member a role."""
await ctx.respond(f"/{ctx.command_name}: {member.mention} gets {role.mention}.")
@colors.command(name="set")
async def set_color(ctx: Context, member: ResolvedMember, hex_code: str) -> None:
"""Give a member a color role."""
await ctx.respond(f"/{ctx.command_name}: {member.mention} is now {hex_code}.")
Так определяются /roles give и /roles color set. @group.command(name=...) даёт подкоманде имя, отличное от имени функции. ctx.command_name — полное имя выполненной команды, например "roles color set", а command.qualified_name — то же самое для объекта команды.
Группа верхнего уровня принимает настройки команды, кроме name, ephemeral и auto_defer, и они действуют на все её подкоманды: default_permissions, nsfw, guilds, contexts и integration_types в Discord существуют только у команд верхнего уровня. Вложенная группа принимает обе локализации, checks и extra. Подкоманда принимает name, description, обе локализации, ephemeral, auto_defer, checks и extra. Проверки группы выполняются до проверок её подкоманд.
Настройки команды¶
import bostie
from bostie import (
ApplicationIntegrationType,
Context,
InteractionContextType,
Permissions,
)
bot = bostie.Bot()
@bot.command(
name="purge",
description="Delete recent messages",
name_localizations={"es-ES": "purgar"},
description_localizations={"es-ES": "Borra mensajes recientes"},
default_permissions=Permissions.MANAGE_MESSAGES,
contexts=[InteractionContextType.GUILD],
integration_types=[ApplicationIntegrationType.GUILD_INSTALL],
ephemeral=True,
auto_defer=1.0,
)
async def purge_messages(ctx: Context, count: int = 10) -> None:
await ctx.respond(f"Deleting {count} messages.")
@bot.command(guilds=[1335172792320131073], nsfw=True)
async def staff(ctx: Context) -> None:
"""Registered in one guild only, and shown in age-restricted channels only."""
await ctx.respond("Staff only.")
| Настройка | Значение |
|---|---|
name, description |
имя и описание вместо взятых из функции |
name_localizations, description_localizations |
текст для каждой локали; функция перевода описана в «Регистрации команд» |
ephemeral |
ответы видит только пользователь, если вызов не указывает иное |
auto_defer |
через сколько секунд ответ на неотвеченное использование откладывается; None отключает это («Ответы на взаимодействия») |
default_permissions |
права, которые нужны участнику, чтобы видеть и использовать команду; администраторы сервера могут изменить, кому она доступна |
nsfw |
команда видна только в каналах с возрастным ограничением |
guilds |
ID серверов, на которых команда регистрируется вместо глобальной регистрации |
contexts |
где можно использовать глобальную команду: на серверах, в личных сообщениях с ботом, в приватных каналах |
integration_types |
в каких установках есть команда: установка на сервер, установка для пользователя |
checks |
проверки, которые выполняются до обработчика («Проверки») |
extra |
сырые поля, которые добавляются в JSON команды |
ephemeral, auto_defer, default_permissions, nsfw, contexts и integration_types можно также задать для всех команд роутера, а contexts, integration_types и auto_defer — для всех команд бота.
Лимиты¶
Декоратор проверяет то, что отклонил бы Discord, и выбрасывает CommandDefinitionError с именем функции и параметра:
- Имена — от 1 до 32 символов из букв, цифр,
-и_, без заглавных букв. Имена команд пользователя и сообщения могут также содержать заглавные буквы и пробелы. - Описания — от 1 до 100 символов.
- У команды не больше 25 параметров, у параметра — не больше 25 вариантов.
*args,**kwargs, параметры только для позиционной передачи и параметры без аннотации типа не принимаются.
Если имена, описания и значения вариантов команды в сумме превышают разрешённые Discord 8000 символов, об этом сообщает предупреждение, а не ошибка. Синхронизация предупреждает, когда в одной области больше 100 слэш-команд, 15 команд пользователя или 15 команд сообщения.
Дальше¶
- Команды пользователя, сообщения и точки входа
- Регистрация команд в Discord, на одном сервере или глобально, и их перевод
- Ответы на взаимодействия