Models and enums¶
Everything bostie decodes from Discord is a model: Message, Guild, Member, Interaction and about two hundred more, all importable from bostie. Models are frozen msgspec structs, decoded straight from the JSON bytes.
import msgspec
import bostie
from bostie import (
Context,
Embed,
EmbedField,
Message,
MessageType,
WebhookMessage,
fmt,
)
bot = bostie.Bot()
@bot.message_command("Inspect")
async def inspect(ctx: Context, message: Message | WebhookMessage) -> None:
if isinstance(message, WebhookMessage):
sender = f"the webhook {message.author.username}"
else:
sender = message.author.mention
kind = message.type.name
if message.type.name.startswith("UNKNOWN_"):
kind = f"a type bostie does not know yet ({message.type.value})"
embed = Embed(
title="Message",
url=message.jump_url,
fields=(
EmbedField(name="Sent by", value=sender),
EmbedField(name="Type", value=kind),
EmbedField(name="Deletable", value=str(message.type.deletable)),
EmbedField(name="Created", value=fmt.timestamp(message.created_at)),
EmbedField(name="Raw keys", value=", ".join(sorted(message.raw or {}))),
),
)
if message.type is MessageType.REPLY:
embed = msgspec.structs.replace(embed, description="A reply.")
await ctx.respond(embeds=[embed], ephemeral=True)
Types follow Discord's documentation¶
Each field is typed as Discord's documentation describes it:
- A field Discord always sends has no default and is never missing. If the docs say it can be
null, it isT | None. - A field the docs mark optional has a default:
()for a list, an empty flag set for flags,Falsefor a boolean whose absence means false, andNoneotherwise. - Ids are
int, timestamps are timezone-awaredatetimeobjects in UTC, and colors areint. - Where Discord sends a partial object, the field has a partial model:
PartialUser,PartialMember,PartialChannel,PartialMessage,PartialEmoji,PartialApplication,PartialIntegration,PartialEntitlement,UnavailableGuild. A partial model has only the fields the partial payload has, so a missing field is a type error, not aNoneat run time.
Three places where one payload has two shapes get two classes:
- A message sent by a webhook, interaction answers included, is a
WebhookMessage, whoseauthoris aPartialUser. Any other message is aMessagewith a fullUserauthor. Fields that accept both are typedMessage | WebhookMessage. - A member resolved from an option, a select or a command target is a
ResolvedMember, which carries itsuserand has nodeafandmute. AMemberfrom the gateway or REST has both. - A user in
message.mentionsis aMentionedUser: aUserwith thememberDiscord adds when the message was sent in a guild.
Interactions are CommandInteraction, ComponentInteraction, ModalSubmitInteraction or AutocompleteInteraction by their type, each with typed data.
Change resilience¶
Discord adds fields, enum values and component types without notice, and a bot must not break when it does:
- Fields bostie does not know are ignored when decoding.
- Enums map an unknown value to a stable member named
UNKNOWN_<value>, which compares equal to the value:MessageType(99).name == "UNKNOWN_99". - Flags keep bits bostie does not know, through every operator,
~included. - An unknown component type decodes to
UnknownComponent, which keeps its raw JSON and is sent back unchanged when you pass it incomponents; an unknown interaction type decodes toUnknownInteraction, which reachesInteractionCreatelisteners but no command or route. - A nested message, such as
message.referenced_messageor the message of a component interaction, is decoded only when you read the property, so a broken one fails only where it is read.
An event payload that does not fit its class at all, for example because a required field is missing, is logged once and delivered only to RawEvent listeners, as Events explains.
Raw payloads¶
A model decoded from a whole payload keeps it: model.raw_json holds the bytes and model.raw decodes them into a new dict. Nested models, such as the author of a message, have raw_json = None. Every event has raw and raw_json too. They reach any field Discord sends before bostie models it.
A model's repr leaves out raw_json and secrets: token, access_token, refresh_token and id_token.
Building and copying models¶
Models are immutable, and built with keyword arguments. msgspec.structs.replace(model, field=value) returns a copy with some fields changed. The models you send to Discord, such as Embed, AllowedMentions, PollCreateRequest, MessageReference, ForumTag, RecurrenceRule and AutoModerationAction, leave out the fields you did not set, so a decoded one is sent back as it came.
Helpers on models¶
| Helper | Value |
|---|---|
user.mention, member.mention, role.mention, channel.mention |
the mention text, such as <@123> |
user.display_name, member.display_name |
the global name or username; for a member, the nickname first |
message.jump_url |
the link to the message |
message.author_member |
the author as a ResolvedMember, when the message carries the member |
role.permissions, overwrite.allow, overwrite.deny |
Permissions flags |
member.permissions |
a member's permissions in the interaction's channel, when Discord sent them |
webhook.url |
the webhook's URL, when it has a token |
The formatting guide covers mentions, timestamps and CDN URLs built from ids.
Snowflakes¶
Discord ids encode when the object was created. Every model with a snowflake id has created_at, that moment as a UTC datetime: message.created_at, member.created_at (the account's), emoji.created_at (None for a Unicode emoji). bostie.snowflake_time(id) does the same for a bare id, and bostie.time_snowflake(when) the lowest id created at a moment, or with high=True the highest, for paging by date. A naive datetime is read as local time.
Enums¶
bostie exports Discord's enums and flags: ChannelType, MessageType, Permissions, Intents, MessageFlags, Locale, ButtonStyle and the others in the enums reference. Flags combine with | and test with in: Permissions.SEND_MESSAGES in member_permissions. message.type.deletable says whether Discord lets a message of that type be deleted: the group DM and call notices, channel name and icon changes, and a thread's starter message cannot be. JSONErrorCode names Discord's error codes, as HTTPException.code reports them.