Interações por HTTP e eventos de webhook¶
O Discord pode enviar as interações para uma URL do seu app em vez do gateway, e envia alguns eventos, os eventos de webhook, só para uma URL. ASGIApp serve os dois a partir do mesmo bot, com os seus comandos, rotas e listeners sem mudanças.
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) é uma aplicação ASGI: sirva app com qualquer servidor ASGI, atrás de HTTPS. Depois, no Developer Portal, defina a Interactions Endpoint URL do app e, para eventos de webhook, a Webhook Events URL dele, com os tipos de evento que você quer, apontando para onde ele escuta. O Discord verifica um endpoint quando você o salva, com um PING e uma requisição com uma assinatura errada, e ASGIApp responde aos dois como o Discord espera.
O que ele faz¶
Quando o servidor inicia, pelo lifespan do ASGI, o app chama bot.open(), a menos que o bot já esteja rodando no gateway, o que:
- abre o cliente HTTP do bot;
- busca o aplicativo, pelo ID dele e pela chave pública que assina as requisições do Discord, e o usuário do bot;
- envia
Starting, sincroniza os comandos e inicia as tarefas em segundo plano.
Uma falha ali interrompe a inicialização do servidor com o erro. Quando o servidor desliga, o app fecha o bot se foi ele que o iniciou.
Cada requisição é verificada antes de qualquer outra coisa: só POST é aceito (405 caso contrário), os corpos têm limite de 4 MiB (413), e uma requisição cuja X-Signature-Ed25519 não confere com a chave pública recebe 401. Depois:
- Um PING de interação recebe o PONG que o Discord espera.
- Qualquer outra interação é respondida com 202 e entregue ao bot, que roda o seu handler de comando, de rota ou de modal como faria pelo gateway; o handler responde pela API REST do Discord, com
ctx.responde o resto. O adiamento automático funciona do mesmo jeito. - Um evento de webhook chega aos listeners do bot e recebe 204, assim como o PING que o Discord envia para a Webhook Events URL.
ASGIApp(bot, token=..., public_key=...) aceita o token, ou então BOSTIE_TOKEN, e a chave pública, ou então a que o aplicativo informa.
Com ou sem o gateway¶
Servido sozinho, o app não tem conexão com o gateway: nada de eventos do gateway, de atualizações do cache nem de voz. Comandos, componentes, modais, REST e eventos de webhook funcionam. Para ter os dois, inicie o bot também no gateway, no mesmo event loop, e o app passa a usar o bot em execução; as interações então chegam por HTTP e o gateway continua entregando eventos.
Eventos de webhook¶
| Evento | Enviado quando |
|---|---|
ApplicationAuthorized |
um usuário adicionou o app a um servidor ou à sua conta |
ApplicationDeauthorized |
um usuário removeu o app, ou os tokens OAuth2 dele terminaram |
EntitlementCreate, EntitlementUpdate, EntitlementDelete |
um entitlement mudou |
QuestUserEnrollment |
um usuário foi adicionado a uma Missão; o Discord o documenta, mas ainda não o envia aos apps |
LobbyMessageCreate, LobbyMessageUpdate, LobbyMessageDelete |
uma mensagem foi enviada, editada ou excluída em um lobby do Social SDK |
GameDirectMessageCreate, GameDirectMessageUpdate, GameDirectMessageDelete |
uma mensagem direta em uma sessão do Social SDK mudou |
Escute-os com @bot.listen como os eventos do gateway. Um tipo de evento de webhook que o bostie não modela chega como um RawEvent.
No seu próprio servidor web¶
As duas peças que ASGIApp usa são públicas, para um framework web que você já roda:
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) confere os cabeçalhos X-Signature-Ed25519 e X-Signature-Timestamp de uma requisição com a chave pública em hexadecimal do app; responda 401 quando ela retornar False. decode_webhook_event(body) retorna o evento tipado de uma requisição de Webhook Events, um RawEvent para um tipo que o bostie não modela, ou None para o PING. Para interações no seu próprio servidor, o exemplo de interações por HTTP mostra o caminho ASGI, que é o que o bostie suporta.