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

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(), если бот ещё не работает через шлюз; этот вызов:

  1. открывает HTTP-клиент бота;
  2. получает приложение — ради его ID и открытого ключа, которым подписаны запросы Discord, — и пользователя бота;
  3. отправляет 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.