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

Ответы на взаимодействия

Каждая слэш-команда, команда контекстного меню, кнопка, меню выбора и отправка модального окна — это взаимодействие, и Discord ждёт, пока приложение на него ответит. Обработчик команды получает Context, обработчик компонента — ComponentContext, а обработчик модального окна — ModalContext; у них общие методы ответа, описанные ниже.

Что знает контекст

Атрибут Значение
ctx.user User, который вызвал взаимодействие, на сервере или в личных сообщениях
ctx.member его Member на сервере, иначе None
ctx.guild_id, ctx.channel_id где это произошло; в личных сообщениях guild_id равен None
ctx.channel PartialChannel, который несёт взаимодействие
ctx.locale язык клиента пользователя
ctx.interaction вся модель взаимодействия — для всего остального, например app_permissions или guild_locale
ctx.bot бот
ctx.voice голос для этого сервера и пользователя
ctx.command_name в Context — команда вместе с подкомандами, например "roles color set"

Ответить, изменить, дополнить

import asyncio

import bostie
from bostie import Context

bot = bostie.Bot()


@bot.command
async def countdown(ctx: Context, seconds: int = 3) -> None:
    """Count down in one message."""
    await ctx.respond(f"{seconds}…")
    for left in range(seconds - 1, 0, -1):
        await asyncio.sleep(1)
        await ctx.edit(content=f"{left}…")
    await asyncio.sleep(1)
    await ctx.edit(content="Go!")
    note = await ctx.followup("Only you see this one.", ephemeral=True)
    await asyncio.sleep(5)
    await ctx.delete(note.id)

ctx.respond(...) отправляет первый ответ. При повторном вызове он отправляет последующее сообщение, так что обработчик может вызывать его сколько угодно. Он возвращает сообщение, созданное Discord, — WebhookMessage.

  • ctx.edit(**parts) изменяет первый ответ, а ctx.edit(message_id, **parts) — последующее сообщение.
  • ctx.followup(...) всегда отправляет новое сообщение, кроме вызова сразу после defer(), когда он заполняет отложенный ответ.
  • ctx.fetch() и ctx.fetch(message_id) возвращают ответ или последующее сообщение в том виде, в каком они сейчас есть в Discord.
  • ctx.delete() и ctx.delete(message_id) удаляют их.

respond и followup принимают content и части, перечисленные в разделе «Содержимое сообщений»: embeds, components, files, allowed_mentions, flags, tts, poll, а также ephemeral. Изменение принимает те же части как именованные аргументы: часть, которую вы не передали, остаётся как есть, а None её очищает.

Сообщения, которые удаляются сами

import bostie
from bostie import Context

bot = bostie.Bot()


@bot.command
async def nudge(ctx: Context) -> None:
    """Remind the channel to stay on topic."""
    await ctx.respond("Please keep this channel on topic.", delete_after=30)

delete_after= в respond, followup и bot.rest.messages.create удаляет отправленное сообщение через указанное число секунд. Бот выполняет удаление в фоне и отменяет его при закрытии, так что сообщение, срок которого наступает после остановки бота, остаётся. Сообщения взаимодействия удаляются через его токен — это работает и для эфемерных, но перестаёт работать через 15 минут после взаимодействия; более позднее удаление завершается неудачей с предупреждением в логе. Сообщение, которое кто-то уже удалил, тихо пропускается.

Эфемерные ответы

ephemeral=True в вызове делает это сообщение видимым только пользователю. ephemeral=True на команде, маршруте компонента, маршруте модального окна или роутере делает это поведением по умолчанию для их ответов, а вызов всё равно может передать ephemeral=False. Существующее сообщение не может стать из эфемерного публичным или наоборот: изменение, которое этого требует, один раз пишет предупреждение в лог и оставляет сообщение как есть.

Откладывание ответа

Первый ответ должен дойти до Discord за 3 секунды. await ctx.defer() показывает на его месте «думает…», и тогда у обработчика есть 15 минут, чтобы вызвать respond; defer(ephemeral=True) делает будущий ответ эфемерным.

import asyncio

import bostie
from bostie import Context

bot = bostie.Bot(auto_defer=2.0)


@bot.command
async def report(ctx: Context) -> None:
    """Build a slow report."""
    await ctx.defer(ephemeral=True)
    await asyncio.sleep(10)
    await ctx.respond("The report is ready.")


@bot.command(auto_defer=None)
async def instant(ctx: Context) -> None:
    """Never deferred automatically."""
    await ctx.respond("Done.")

bostie откладывает ответ и сам: использование, на которое не ответили через auto_defer секунд после его прихода, откладывается; по умолчанию это 2,5 секунды. Компонент или модальное окно, открытые из сообщения, откладываются через defer_update, который оставляет сообщение как есть, пока update его не изменит. auto_defer задаётся на боте, роутере, команде или маршруте, а None отключает его там. Таймер отсчитывается с момента, когда шлюз доставил взаимодействие, поэтому заблокированный цикл событий задерживает обработчик, но не крайний срок; bostie пишет предупреждение в лог, когда обработчик запускается с опозданием больше чем на секунду.

Модальные окна и Активности

await ctx.modal(ui.modal(...)) отвечает всплывающей формой; см. «Модальные окна». await ctx.launch_activity() открывает Активность приложения и возвращает ID её экземпляра. Оба вызова должны быть первым ответом, и после модального окна на взаимодействие уже ничем не ответить: отправка модального окна — это новое взаимодействие.

Правила и ошибки при их нарушении

Каждый вызов сначала проверяет состояние взаимодействия:

Вызов До любого ответа После defer() После defer_update() После ответа
respond отвечает заполняет ответ отправляет последующее сообщение отправляет последующее сообщение
defer откладывает ничего ничего InteractionResponded
edit, delete InteractionNotAnswered изменяет, удаляет ответ изменяет, удаляет сообщение изменяет, удаляет ответ
followup InteractionNotAnswered заполняет ответ отправляет последующее сообщение отправляет последующее сообщение
update изменяет сообщение InteractionResponded изменяет сообщение InteractionResponded
modal, launch_activity отвечает InteractionResponded InteractionResponded InteractionResponded

update и defer_update есть только в контекстах компонентов и модальных окон. Любой вызов после модального окна вызывает InteractionResponded.

Токен взаимодействия живёт 15 минут. Любой вызов после этого или после того, как Discord сообщил, что токен недействителен, вызывает InteractionExpired и ничего не отправляет.

Если обработчик завершился, не ответив, пользователь видит «Приложение не ответило», а bostie пишет в лог предупреждение с именем обработчика. Обработчик, который отложил ответ и так и не ответил, оставляет «думает…» до истечения токена, тоже с предупреждением. Если обработчик выбросил исключение до ответа, bostie отвечает «Something went wrong.» — эфемерно, если обработчик не откладывал ответ, — если только не ответит хук ошибок.

Discord разрешает 5 последующих сообщений для взаимодействия приложения, установленного только для пользователя, и bostie предупреждает на шестом.

Разрешённые упоминания

По умолчанию ответ может пинговать пользователей и роли, но не @everyone и не @here: Bot(allowed_mentions=...) равен AllowedMentions(parse=("users", "roles")). allowed_mentions, переданный с сообщением, меняет только те виды упоминаний, которые задаёт, поэтому AllowedMentions(users=(123,)) ограничивает пинги пользователей и не трогает настройку ролей. AllowedMentions(parse=()) глушит все упоминания, а replied_user=False не даёт ответу пинговать автора сообщения, на которое он отвечает.