Saltar a contenido

Interacciones por HTTP y eventos de webhook

Discord puede enviar las interacciones a una URL de tu aplicación en lugar de al gateway, y algunos eventos, los eventos de webhook, los envía solo a una URL. ASGIApp sirve los dos desde el mismo bot, con sus comandos, rutas y listeners sin cambios.

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) es una aplicación ASGI: sirve app con cualquier servidor ASGI, detrás de HTTPS. Luego, en el Developer Portal, configura la Interactions Endpoint URL de la aplicación y, para los eventos de webhook, su Webhook Events URL, con los tipos de evento que quieras, apuntando a donde escucha. Discord comprueba un endpoint cuando lo guardas, con un PING y una solicitud con una firma incorrecta, y ASGIApp responde a los dos como espera Discord.

Qué hace

Cuando arranca el servidor, mediante el lifespan de ASGI, la aplicación llama a bot.open(), salvo que el bot ya esté ejecutándose en el gateway, y eso:

  1. abre el cliente HTTP del bot;
  2. obtiene la aplicación, para conocer su id y la clave pública que firma las solicitudes de Discord, y el usuario del bot;
  3. envía Starting, sincroniza los comandos e inicia las tareas en segundo plano.

Un fallo en ese punto detiene el arranque del servidor con el error. Cuando el servidor se apaga, la aplicación cierra el bot si fue ella quien lo inició.

Cada solicitud se comprueba antes que nada: solo se acepta POST (si no, 405), los cuerpos tienen un límite de 4 MiB (413), y una solicitud cuyo X-Signature-Ed25519 no se verifica con la clave pública recibe 401. Después:

  • Un PING de interacción recibe el PONG que espera Discord.
  • Cualquier otra interacción se responde con 202 y se le pasa al bot, que ejecuta su handler de comando, de ruta o de modal como lo haría con el gateway; el handler responde a través de la API REST de Discord, con ctx.respond y los demás. El diferimiento automático funciona igual.
  • Un evento de webhook llega a los listeners del bot y recibe 204, igual que el PING que Discord envía a la Webhook Events URL.

ASGIApp(bot, token=..., public_key=...) acepta el token, o si no BOSTIE_TOKEN, y la clave pública, o si no la que informa la aplicación.

Con o sin el gateway

Si se sirve sola, la aplicación no tiene conexión con el gateway: no hay eventos del gateway, ni actualizaciones de la caché, ni voz. Funcionan los comandos, los componentes, los modales, REST y los eventos de webhook. Para tener las dos cosas, inicia también el bot en el gateway, en el mismo event loop, y la aplicación usará entonces el bot en ejecución; las interacciones llegan por HTTP y el gateway sigue entregando eventos.

Eventos de webhook

Evento Se envía cuando
ApplicationAuthorized un usuario agregó la aplicación a un servidor o a su cuenta
ApplicationDeauthorized un usuario quitó la aplicación, o vencieron sus tokens de OAuth2
EntitlementCreate, EntitlementUpdate, EntitlementDelete cambió un entitlement
QuestUserEnrollment se agregó un usuario a una Misión; Discord lo documenta pero todavía no se lo envía a las aplicaciones
LobbyMessageCreate, LobbyMessageUpdate, LobbyMessageDelete se envió, editó o eliminó un mensaje en un lobby del Social SDK
GameDirectMessageCreate, GameDirectMessageUpdate, GameDirectMessageDelete cambió un mensaje directo en una sesión del Social SDK

Escúchalos con @bot.listen igual que los eventos del gateway. Un tipo de evento de webhook que bostie no modela llega como un RawEvent.

En tu propio servidor web

Las dos piezas que usa ASGIApp son públicas, para un framework web que ya tengas en marcha:

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) comprueba las cabeceras X-Signature-Ed25519 y X-Signature-Timestamp de una solicitud con la clave pública hexadecimal de la aplicación; responde 401 cuando devuelve False. decode_webhook_event(body) devuelve el evento tipado de una solicitud de Webhook Events, un RawEvent para un tipo que bostie no modela, o None para el PING. Para las interacciones en tu propio servidor, el ejemplo de interacciones por HTTP muestra la vía ASGI, que es la que admite bostie.