Ответы на взаимодействия¶
Каждая слэш-команда, команда контекстного меню, кнопка, меню выбора и отправка модального окна — это взаимодействие, и 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 не даёт ответу пинговать автора сообщения, на которое он отвечает.