HTTP-взаимодействия и события вебхуков¶
Discord может присылать взаимодействия не через шлюз, а на URL вашего приложения, а некоторые события — события вебхуков — он присылает только на URL. ASGIApp обслуживает и то и другое из одного и того же бота, с теми же командами, маршрутами и слушателями.
import logging
import bostie
from bostie import Context
from bostie.events import ApplicationAuthorized, EntitlementCreate
log = logging.getLogger("my-bot")
bot = bostie.Bot()
app = bostie.ASGIApp(bot)
@bot.command
async def ping(ctx: Context) -> None:
"""Answer over HTTP."""
await ctx.respond("pong")
@bot.listen
async def installed(event: ApplicationAuthorized) -> None:
where = "a guild" if event.guild is not None else "a user account"
log.info("%s added the app to %s", event.user.username, where)
@bot.listen
async def purchased(event: EntitlementCreate) -> None:
log.info("new entitlement %d", event.entitlement.id)
bostie.ASGIApp(bot) — это ASGI-приложение: обслуживайте app любым ASGI-сервером за HTTPS. Затем в Developer Portal укажите адрес, который слушает приложение, в поле Interactions Endpoint URL, а для событий вебхуков — в поле Webhook Events URL, выбрав нужные типы событий. Discord проверяет эндпоинт при сохранении — запросом PING и запросом с неверной подписью, — и ASGIApp отвечает на оба так, как ожидает Discord.
Что оно делает¶
При запуске сервера, через lifespan ASGI, приложение вызывает bot.open(), если бот ещё не работает через шлюз; этот вызов:
- открывает HTTP-клиент бота;
- получает приложение — ради его ID и открытого ключа, которым подписаны запросы Discord, — и пользователя бота;
- отправляет
Starting, синхронизирует команды и запускает фоновые задачи.
Сбой на этом этапе останавливает запуск сервера с ошибкой. При остановке сервера приложение закрывает бота, если само его запустило.
Каждый запрос сначала проверяется: принимается только POST (иначе 405), размер тела ограничен 4 МиБ (413), а запрос, чья X-Signature-Ed25519 не проходит проверку открытым ключом, получает 401. Затем:
- PING взаимодействия получает PONG, которого ждёт Discord.
- На любое другое взаимодействие отправляется ответ 202, и оно передаётся боту, который запускает обработчик команды, маршрута или модального окна так же, как для шлюза; обработчик отвечает через REST API Discord с помощью
ctx.respondи остальных методов. Автоматическое откладывание ответа работает так же. - Событие вебхука доходит до слушателей бота и получает 204, как и PING, который Discord присылает на Webhook Events URL.
ASGIApp(bot, token=..., public_key=...) принимает токен (иначе — BOSTIE_TOKEN) и открытый ключ (иначе — тот, который сообщает приложение).
Со шлюзом или без¶
Если приложение работает само по себе, у него нет соединения со шлюзом: нет событий шлюза, обновлений кэша и голоса. Команды, компоненты, модальные окна, REST и события вебхуков работают. Чтобы получить и то и другое, запустите бота ещё и через шлюз в том же цикле событий, и тогда приложение использует уже работающего бота; взаимодействия при этом приходят по HTTP, а шлюз по-прежнему доставляет события.
События вебхуков¶
| Событие | Когда отправляется |
|---|---|
ApplicationAuthorized |
пользователь добавил приложение на сервер или в свой аккаунт |
ApplicationDeauthorized |
пользователь удалил приложение или истекли его токены OAuth2 |
EntitlementCreate, EntitlementUpdate, EntitlementDelete |
изменился entitlement |
QuestUserEnrollment |
пользователя добавили в задание (Quest); Discord его документирует, но пока не присылает приложениям |
LobbyMessageCreate, LobbyMessageUpdate, LobbyMessageDelete |
сообщение было отправлено, изменено или удалено в лобби Social SDK |
GameDirectMessageCreate, GameDirectMessageUpdate, GameDirectMessageDelete |
изменилось личное сообщение в сессии Social SDK |
Слушайте их через @bot.listen, как события шлюза. Тип события вебхука, который bostie не моделирует, приходит как RawEvent.
В вашем собственном веб-сервере¶
Две части, которые использует ASGIApp, публичны — для веб-фреймворка, который у вас уже работает:
import logging
import os
from collections.abc import Mapping
from bostie import decode_webhook_event, verify_signature
from bostie.events import ApplicationDeauthorized, RawEvent
log = logging.getLogger("my-server")
PUBLIC_KEY = os.environ.get("DISCORD_PUBLIC_KEY", "")
def webhook_event(headers: Mapping[str, str], body: bytes) -> int:
signature = headers.get("x-signature-ed25519", "")
timestamp = headers.get("x-signature-timestamp", "")
if not verify_signature(PUBLIC_KEY, signature, timestamp, body):
return 401
event = decode_webhook_event(body)
if isinstance(event, ApplicationDeauthorized):
log.info("%s removed the app", event.user.username)
elif isinstance(event, RawEvent):
log.info("an event bostie does not model: %s", event.name)
return 204
verify_signature(public_key, signature, timestamp, body) проверяет заголовки запроса X-Signature-Ed25519 и X-Signature-Timestamp по открытому ключу приложения в шестнадцатеричном виде; если функция возвращает False, отвечайте 401. decode_webhook_event(body) возвращает типизированное событие запроса Webhook Events, RawEvent для типа, который bostie не моделирует, или None для PING. Для взаимодействий в собственном сервере пример HTTP-взаимодействий показывает способ через ASGI — именно его поддерживает bostie.