跳转至

消息内容

无论从哪里发送,消息都由同样的部分构成:ctx.respond、ctx.followup、ctx.edit 和 ctx.update,bot.rest.messages.create 和 edit,以及 Webhook 的 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)都是使用关键字参数构造的不可变模型;按 Discord JSON 结构写的普通 dict 也可以。color 接受一个 int,例如 0x5865F2,也可以用 bostie.color 中的辅助函数构建。

File(name, data) 以 name 为文件名上传 data。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 Intent,网关指南中有详细说明。

正在输入提示

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) 的作用相同,可以直接 await,也可以用在 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 客户端录制的是 48 kHz 单声道 Ogg Opus。语音消息只能包含这一个文件,不能有其他任何东西:不能有文字内容、嵌入内容、组件、投票或贴纸。bostie 会设置 IS_VOICE_MESSAGE 标志,如果消息中还有其他内容则引发 ValueError。你自己构造的 File 只要带有 duration_secs 和 waveform(Discord 绘制的 8 位采样值),就会成为语音消息。