HTTP インタラクションとウェブフックイベント¶
Discord は、インタラクションをゲートウェイの代わりにアプリの URL へ送ることができます。また、一部のイベント(ウェブフックイベント)は URL にしか送りません。ASGIApp は、その両方を同じ Bot で処理します。コマンド、ルート、リスナーは変更不要です。
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 アプリケーションです。任意の ASGI サーバーで、HTTPS の背後で app を公開してください。次に Developer Portal で、アプリの Interactions Endpoint URL と、ウェブフックイベントを使う場合は Webhook Events URL を、受け取りたいイベントの種類とともに、アプリが待ち受けている場所に設定します。Discord はエンドポイントを保存する時点で、PING と、署名が誤ったリクエストを送って確認します。ASGIApp はその両方に Discord が期待するとおりに応答します。
何をするか¶
サーバーが ASGI の lifespan を通じて起動すると、Bot がすでにゲートウェイで動いている場合を除き、アプリは bot.open() を呼び出します。これは次の処理を行います。
- Bot の HTTP クライアントを開きます。
- アプリケーション(その ID と、Discord のリクエストに署名する公開鍵を得るため)と、Bot のユーザーを取得します。
Startingを送り、コマンドを同期し、バックグラウンドタスクを開始します。
この段階で失敗すると、サーバーの起動はそのエラーで中止されます。サーバーが終了するとき、アプリは自分が起動した Bot であれば閉じます。
各リクエストは、何よりも先に検査されます。受け付けるのは POST だけで(それ以外は 405)、本文の上限は 4 MiB(超えると 413)で、X-Signature-Ed25519 が公開鍵で検証できないリクエストには 401 を返します。そのうえで、
- インタラクションの PING には、Discord が期待する PONG を返します。
- それ以外のインタラクションには 202 で応答し、Bot に渡します。Bot はゲートウェイの場合と同じようにコマンド、ルート、モーダルのハンドラーを実行し、ハンドラーは
ctx.respondなどを使って Discord の REST API 経由で応答します。自動の遅延応答も同じように動作します。 - ウェブフックイベントは Bot のリスナーに届き、204 を返します。Discord が Webhook Events URL に送る PING にも 204 を返します。
ASGIApp(bot, token=..., public_key=...) はトークン(省略時は BOSTIE_TOKEN)と公開鍵(省略時はアプリケーションが報告する公開鍵)を受け取ります。
ゲートウェイの有無¶
アプリを単独で公開した場合、ゲートウェイ接続はありません。ゲートウェイイベントも、キャッシュの更新も、ボイスもありません。コマンド、コンポーネント、モーダル、REST、ウェブフックイベントは動作します。両方を使うには、同じイベントループで Bot をゲートウェイでも起動します。するとアプリは動作中の Bot を使うようになり、インタラクションは HTTP で届き、ゲートウェイは引き続きイベントを配信します。
ウェブフックイベント¶
| イベント | 送られるタイミング |
|---|---|
ApplicationAuthorized |
ユーザーがアプリをサーバーまたは自分のアカウントに追加したとき |
ApplicationDeauthorized |
ユーザーがアプリを削除したとき、またはアプリの OAuth2 トークンが失効したとき |
EntitlementCreate、EntitlementUpdate、EntitlementDelete |
エンタイトルメントが変更されたとき |
QuestUserEnrollment |
ユーザーがクエストに追加されたとき。Discord はこれを文書化していますが、まだアプリには送っていません |
LobbyMessageCreate、LobbyMessageUpdate、LobbyMessageDelete |
Social SDK のロビーでメッセージが送信、編集、削除されたとき |
GameDirectMessageCreate、GameDirectMessageUpdate、GameDirectMessageDelete |
Social SDK のセッション内のダイレクトメッセージが変更されたとき |
ゲートウェイイベントと同じく @bot.listen で受け取ります。bostie がモデル化していない種類のウェブフックイベントは RawEvent として届きます。
独自の Web サーバーで使う¶
ASGIApp が使っている 2 つの部品は公開されているので、すでに動かしている Web フレームワークでも使えます。
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 ヘッダーを、アプリの 16 進数の公開鍵で検証します。False が返ったら 401 で応答してください。decode_webhook_event(body) は、Webhook Events のリクエストから型付きのイベントを返します。bostie がモデル化していない種類なら RawEvent を、PING なら None を返します。独自のサーバーでインタラクションを扱う場合は、HTTP インタラクションのサンプルが ASGI での方法を示しています。bostie がサポートしているのはこの方法です。