Перейти к содержанию

Содержимое сообщений

Сообщение собирается из одних и тех же частей, куда бы вы его ни отправляли: ctx.respond, ctx.followup, ctx.edit и ctx.update, bot.rest.messages.create и edit, а также execute вебхука. Каждая часть — именованный аргумент, а часть, которую вы не передали, не отправляется.

Часть Тип Примечания
content str первый позиционный аргумент respond, followup, messages.create и execute
embeds объекты Embed до 10 эмбедов
components конструкторы ui кнопки, меню выбора и компоненты разметки; см. «Компоненты»
files объекты File загрузки; см. ниже
allowed_mentions AllowedMentions какие упоминания могут пинговать; см. «Разрешённые упоминания»
flags MessageFlags например SUPPRESS_EMBEDS и SUPPRESS_NOTIFICATIONS
tts bool зачитать содержимое вслух всем, у кого открыт канал
poll PollCreateRequest опрос; см. ниже
ephemeral bool только для ответов на взаимодействия

bot.rest.messages.create также принимает reply_to, message_reference, sticker_ids, nonce, enforce_nonce и shared_client_theme, а ещё delete_after, как и ответы на взаимодействия.

Эмбеды и файлы

import asyncio
from datetime import UTC, datetime
from pathlib import Path

import bostie
from bostie import (
    AllowedMentions,
    Context,
    Embed,
    EmbedField,
    EmbedFooter,
    EmbedMedia,
    File,
    PollAnswer,
    PollCreateRequest,
    PollMedia,
    color,
)

bot = bostie.Bot()


@bot.command
async def status(ctx: Context) -> None:
    """Post a status report with a chart."""
    data = await asyncio.to_thread(Path("chart.png").read_bytes)
    chart = File("chart.png", data, description="Latency")
    embed = Embed(
        title="Status",
        description="All systems normal.",
        color=color.GREEN,
        timestamp=datetime.now(UTC),
        fields=(
            EmbedField(name="Gateway", value="41 ms", inline=True),
            EmbedField(name="Voice", value="23 ms", inline=True),
        ),
        image=EmbedMedia(url="attachment://chart.png"),
        footer=EmbedFooter(text="Updated every minute"),
    )
    await ctx.respond(embeds=[embed], files=[chart])


@bot.command
async def lunch(ctx: Context) -> None:
    """Ask where to eat."""
    poll = PollCreateRequest(
        question=PollMedia(text="Where do we eat?"),
        answers=(
            PollAnswer(poll_media=PollMedia(text="Pizza")),
            PollAnswer(poll_media=PollMedia(text="Ramen")),
        ),
        duration=2,
    )
    await ctx.respond(poll=poll)


@bot.command
async def shout(ctx: Context, text: str) -> None:
    """Repeat the text, letting @everyone ping."""
    await ctx.respond(text, allowed_mentions=AllowedMentions(parse=("everyone",)))

Embed и его части (EmbedField, EmbedFooter, EmbedAuthor, EmbedMedia) — неизменяемые модели с именованными аргументами; подходит и обычный dict в формате JSON Discord. color принимает int, например 0x5865F2, а собрать его помогают функции из bostie.color.

File(name, data) загружает data под именем name. description — его альтернативный текст, spoiler=True размывает его, а title задаёт заголовок, который показывает Discord. Эмбед или компонент ссылается на загрузку как attachment://name, как диаграмма в примере выше. bostie не проверяет размеры: лимит загрузки в Discord зависит от бустов сервера и Nitro пользователя.

Опросы

PollCreateRequest принимает question и answers — оба типа PollMedia с text и, для ответов, необязательным emoji. duration задаётся в часах, по умолчанию 24, а allow_multiselect=True позволяет пользователю выбрать несколько ответов. bot.rest.polls.end(channel_id, message_id) досрочно завершает опрос, bot.rest.polls.list_answer_voters(...) перечисляет, кто выбрал ответ, а события PollVoteAdd и PollVoteRemove сообщают о голосах по мере их появления.

Изменение и вложения

Изменение принимает части как именованные аргументы, а также attachments. Часть, которую вы не передали, остаётся, а None её очищает: ctx.edit(embeds=None) удаляет эмбеды и оставляет содержимое. Если изменение передаёт files или attachments, в сообщении остаются только вложения, перечисленные в attachments (объектами Attachment или ID), плюс новые файлы.

Components V2

Сообщение, в components которого есть компонент разметки (ui.text, ui.section, ui.container, ui.media_gallery, ui.file, ui.separator или ui.thumbnail), — это сообщение Components V2: bostie ставит флаг IS_COMPONENTS_V2, и компоненты составляют всё сообщение. У такого сообщения не может быть content, embeds, стикеров или опроса, и bostie вызывает ValueError, если вы их передаёте; поместите текст в ui.text. Кнопки и меню выбора, находящиеся только в ui.row, работают и в классических сообщениях с содержимым.

Ответы

import bostie
from bostie import Intents, MessageFlags
from bostie.events import MessageCreate

bot = bostie.Bot(intents=Intents.DEFAULT | Intents.MESSAGE_CONTENT)


@bot.listen
async def thank(event: MessageCreate) -> None:
    message = event.message
    if message.author.bot or "thanks" not in message.content.lower():
        return
    await bot.rest.messages.create(
        message.channel_id,
        "You're welcome!",
        reply_to=message.id,
        flags=MessageFlags.SUPPRESS_NOTIFICATIONS,
    )

reply_to принимает ID сообщения в том же канале. Чтобы переслать сообщение, передайте вместо этого message_reference=MessageReference(type=MessageReferenceType.FORWARD, message_id=..., channel_id=...); одновременно их использовать нельзя. Чтение message.content на серверах требует привилегированного интента Message Content, как объясняет руководство по шлюзу.

Индикатор набора текста

import asyncio

import bostie
from bostie import Context

bot = bostie.Bot()


def weekly_summary() -> str:
    return "12 new members and 340 messages this week."


@bot.command
async def report(ctx: Context) -> None:
    """Write this week's report."""
    await ctx.respond("Writing the report.")
    async with ctx.typing():
        summary = await asyncio.to_thread(weekly_summary)
    await ctx.followup(summary)

async with ctx.typing(): показывает, что бот печатает в канале взаимодействия, пока выполняется блок. bostie отправляет индикатор в начале блока и затем каждые 9 секунд, поскольку Discord показывает его 10 секунд, и прекращает, когда блок заканчивается, в том числе из-за исключения. await ctx.typing() показывает его один раз. Вне взаимодействия то же самое делает bot.rest.channels.trigger_typing_indicator(channel_id) — при ожидании или в async with. Сообщение, которое бот отправляет в канал, сразу убирает индикатор.

Голосовые сообщения

import bostie
from bostie import Context, voice

bot = bostie.Bot()


@bot.command
async def memo(ctx: Context) -> None:
    """Send today's memo as a voice message."""
    await ctx.respond(files=[await voice.message("memo.ogg")])

await voice.message(audio) читает путь к файлу или байты, измеряет длительность, вычисляет волновую форму, которую рисует Discord, и возвращает File, готовый к отправке. Формат определяется по расширению имени; клиенты Discord записывают моно Ogg Opus 48 кГц. Голосовое сообщение — это только этот файл и ничего больше: ни содержимого, ни эмбедов, ни компонентов, ни опроса, ни стикеров. bostie ставит флаг IS_VOICE_MESSAGE и вызывает ValueError, если в сообщении есть что-то ещё. File, который вы собираете сами, становится голосовым сообщением, если у него есть duration_secs и waveform — 8-битные отсчёты, которые рисует Discord.