Message content¶
A message is built from the same parts wherever you send it: ctx.respond, ctx.followup, ctx.edit and ctx.update, bot.rest.messages.create and edit, and a webhook's execute. Each part is a keyword argument, and a part you leave out is not sent.
| Part | Type | Notes |
|---|---|---|
content |
str |
the first positional argument of respond, followup, messages.create and execute |
embeds |
Embed objects |
up to 10 rich embeds |
components |
ui builders |
buttons, selects and the layout components; see Components |
files |
File objects |
uploads; see below |
allowed_mentions |
AllowedMentions |
which mentions may ping; see Allowed mentions |
flags |
MessageFlags |
such as SUPPRESS_EMBEDS and SUPPRESS_NOTIFICATIONS |
tts |
bool |
read the content aloud to whoever has the channel open |
poll |
PollCreateRequest |
a poll; see below |
ephemeral |
bool |
interaction answers only |
bot.rest.messages.create also takes reply_to, message_reference, sticker_ids, nonce, enforce_nonce and shared_client_theme, and delete_after, as interaction answers do.
Embeds and files¶
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 and its parts (EmbedField, EmbedFooter, EmbedAuthor, EmbedMedia) are frozen models with keyword arguments; a plain dict in Discord's JSON shape works too. color takes an int such as 0x5865F2, and the bostie.color helpers build one.
File(name, data) uploads data under name. description is its alt text, spoiler=True blurs it, and title sets the title Discord shows. An embed or a component refers to an upload as attachment://name, as the chart does above. bostie does not check sizes: Discord's upload limit depends on the guild's boosts and the user's Nitro.
Polls¶
PollCreateRequest takes a question and answers, both PollMedia with text and, for answers, an optional emoji. duration is in hours, 24 when left out, and allow_multiselect=True lets a user pick several answers. bot.rest.polls.end(channel_id, message_id) closes a poll early, bot.rest.polls.list_answer_voters(...) lists who picked an answer, and the PollVoteAdd and PollVoteRemove events report votes as they happen.
Editing and attachments¶
An edit takes the parts by keyword, plus attachments. A part you leave out stays, and None clears it: ctx.edit(embeds=None) removes the embeds and keeps the content. When an edit passes files or attachments, the message keeps only the attachments listed in attachments, given as Attachment objects or ids, plus the new files.
Components V2¶
A message whose components include a layout component (ui.text, ui.section, ui.container, ui.media_gallery, ui.file, ui.separator or ui.thumbnail) is a Components V2 message: bostie sets the IS_COMPONENTS_V2 flag and the components are the whole message. Such a message cannot have content, embeds, stickers or a poll, and bostie raises ValueError when you pass them; put the text in ui.text instead. Buttons and selects in ui.row alone work in classic messages with content too.
Replies¶
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 takes the id of a message in the same channel. To forward a message, pass message_reference=MessageReference(type=MessageReferenceType.FORWARD, message_id=..., channel_id=...) instead; the two cannot be combined. Reading message.content in guilds needs the privileged Message Content intent, as the gateway guide explains.
Typing indicator¶
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(): shows the bot typing in the interaction's channel while the block runs. bostie sends the indicator when the block starts and again every 9 seconds, since Discord shows it for 10, and stops when the block ends, by an exception too. await ctx.typing() shows it once. Outside an interaction, bot.rest.channels.trigger_typing_indicator(channel_id) does the same, awaited or in async with. A message the bot sends in the channel clears the indicator at once.
Voice messages¶
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) reads a file path or bytes, measures the duration, computes the waveform Discord draws, and returns a File ready to send. The name's extension tells the format; Discord's clients record 48 kHz mono Ogg Opus. A voice message is that one file and nothing else: no content, embeds, components, poll or stickers. bostie sets the IS_VOICE_MESSAGE flag, and raises ValueError if anything else is in the message. A File you build yourself becomes a voice message when it has duration_secs and waveform, the 8-bit samples Discord draws.