コンテンツにスキップ

メッセージの内容

メッセージは、どこに送る場合でも同じ要素から組み立てます。送り先は 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)は、キーワード引数で作るイミュータブルなモデルです。Discord の JSON と同じ形の素の dict も使えます。color は 0x5865F2 のような int を受け取り、bostie.color のヘルパーでその値を作れます。

File(name, data) は data を name という名前でアップロードします。description は代替テキストで、spoiler=True にするとぼかし表示になり、title は Discord が表示するタイトルを設定します。埋め込みやコンポーネントは、上のグラフの例のように attachment://name でアップロードを参照します。bostie はサイズを確認しません。Discord のアップロード上限は、サーバーのブーストとユーザーの Nitro によって変わります。

投票

PollCreateRequest は question と answers を受け取ります。どちらも text を持つ PollMedia で、回答では任意で 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=...) を渡します。この 2 つは併用できません。サーバーで 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(): は、ブロックの実行中、インタラクションのチャンネルに Bot の入力中の表示を出します。Discord はこの表示を 10 秒間出すので、bostie はブロックの開始時に表示を送り、その後 9 秒ごとに送り直します。ブロックが終わると、例外で終わった場合も含めて送信を止めます。await ctx.typing() は一度だけ表示します。インタラクションの外では、bot.rest.channels.trigger_typing_indicator(channel_id) が同じことを行い、await しても async with で使ってもかまいません。Bot がそのチャンネルにメッセージを送ると、表示はすぐに消えます。

ボイスメッセージ

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 で録音します。ボイスメッセージはその 1 つのファイルだけで構成され、内容、埋め込み、コンポーネント、投票、スタンプは含められません。bostie は IS_VOICE_MESSAGE フラグを設定し、メッセージにほかのものが含まれていると ValueError を送出します。自分で作った File も、duration_secs と、Discord が描く 8 ビットのサンプルである waveform を持っていれば、ボイスメッセージになります。