슬래시 명령어¶

슬래시 명령어는 @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입니다. 이름은 함수 이름이고 설명은 docstring의 첫 줄입니다. @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가 발생합니다. 채널 옵션의 힌트는 Channel이 아니라 PartialChannel이고, 멤버의 힌트는 Member가 아니라 ResolvedMember입니다. 인터랙션에는 이런 부분 객체가 담겨 오기 때문이며, 에러 메시지에서도 이를 알려 줍니다.
필수 옵션과 선택 옵션¶
기본값이 없는 옵션은 필수입니다. 기본값을 주거나 힌트에 | None을 붙이면 선택 옵션이 되고, 사용자가 생략한 선택 옵션은 기본값이나 None을 받습니다. Discord는 필수 옵션이 먼저 오기를 요구하므로, bostie는 매개변수 순서와 관계없이 명령어를 만들 때 필수 옵션을 앞으로 정렬합니다.
설명¶
Annotated 안의 문자열이 옵션의 설명이 됩니다(Annotated[str, "What happened"]). 설명을 docstring에 쓸 수도 있습니다.
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=)보다 우선하고, Option(description=)은 docstring보다 우선합니다. 어디에도 설명이 없는 옵션은 …를 설명으로 받습니다.
범위와 옵션 설정¶
Range[int, 1, 10]은 1부터 10까지(양 끝 포함)의 int 옵션입니다. Range에는 float도 쓸 수 있고, str이면 범위가 길이를 뜻합니다(Range[str, 1, 2000]). 나머지 설정은 모두 Annotated 안의 Option(...)으로 지정하며, 힌트에서 유추되는 내용보다 우선합니다.
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 |
첨부 파일 | image, video, audio 또는 .pdf 같은 확장자, 최대 10개 |
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의 핸들러는 drink로 "tea", "coffee", "water" 중 하나를, size로 Size 멤버를, milk로 선택된 Choice의 값을 받습니다. 힌트가 그냥 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 매개변수의 타입 힌트가 Range와 Annotated 설명까지 포함해 Discord 옵션 타입을 정합니다.
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를 제외한 명령어 설정을 받고, 그 설정은 모든 하위 명령어에 적용됩니다. Discord에서 default_permissions, nsfw, guilds, contexts, integration_types는 최상위 명령어에만 있기 때문입니다. 중첩된 그룹은 두 현지화 필드와 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 |
응답하지 않은 사용을 지연(defer)할 때까지의 초. None이면 끕니다(인터랙션에 응답하기) |
default_permissions |
멤버가 명령어를 보고 쓰는 데 필요한 권한. 서버 관리자는 누가 쓸 수 있는지 바꿀 수 있습니다 |
nsfw |
연령 제한 채널에서만 표시 |
guilds |
전역 대신 명령어를 등록할 서버 ID |
contexts |
전역 명령어를 쓸 수 있는 곳: 서버, 봇과의 개인 메시지(DM), 비공개 채널 |
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에 등록하고 번역하는 방법
- 인터랙션에 응답하기