Discord には「Webhook」と呼ばれる 2 つのものがあり、まったく異なる挙動をします。一つはメッセージ投稿用の撃ちっぱなし URL。もう一つは、ユーザーがスラッシュコマンドを実行するたびに署名付き POST を受け取る Interactions エンドポイントです。前者は localhost で動きます。後者は決して動きません。本ガイドは後者についてです。トンネルが必要なのはそちらだからです。
Discord の Webhook 2 種類
送信 Webhookはあなたが POST する URL です。JSON メッセージを送るとチャンネルに表示されます。トラフィックはあなたのマシンから外向きに流れるためトンネルは不要です。
Interactions エンドポイントは逆方向です。あなたが URL を Discord に登録し、ユーザーが /yourcommand を実行するたびに Discord がそこへ POST します。エンドポイントは HTTPS で、3 秒以内に応答し、すべてのリクエストで Ed25519 署名を検証しなければなりません。localhost だけではどれもできません。
Interactions エンドポイントが特に厳しい理由
Discord は HTTPS を求めるだけでなく、URL を受け入れる前にあなたがエンドポイントを制御していることの証明を要求します。検証は登録時に行われ、Discord が PING インタラクションを送り、あなたのサーバーは署名付き PONG で応答しなければなりません。Ed25519 検証が一度でも失敗すると、Discord は URL の保存を拒否します。
Ed25519 の部分は初回でうまくいきません。ほとんど誰もそうです。
Ed25519 検証、噛みついてくる部分
Discord は(HMAC SHA-256 ではなく)Ed25519 を使い、各インタラクションで 2 つのヘッダーを提供します。
X-Signature-Ed25519— 16 進数の署名X-Signature-Timestamp— 秒単位のタイムスタンプ
開発者ポータルに表示される公開鍵で timestamp + rawBody を署名します。多くの言語にライブラリがあります。Node では:
import nacl from 'tweetnacl';
const valid = nacl.sign.detached.verify(
Buffer.from(timestamp + rawBody),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex'),
);
検証失敗の最大の原因は——またしても——このコードが走る前にボディをパースするミドルウェアです。必要なのは Discord が送った生の文字列であり、パース済みオブジェクトを再文字列化したものではありません。
ステップ・バイ・ステップ:localhost での Interactions エンドポイント
- ボットの HTTP サーバーをローカルで起動します(任意のポート)。
npx portpreview 3000を実行して公開 HTTPS URL を取得します。- Discord 開発者ポータルでアプリを開き、トンネル URL に Interactions のパスを付けて Interactions Endpoint URL に貼り付けます。
- Save を押します。Discord はすぐに
PINGを送ります。Ed25519 検証が通れば URL は保存されます。通らなければフィールドにエラーが出ます。 - 保存後、ボットがいる任意のサーバーでスラッシュコマンドを実行します。リクエストがハンドラーに届きます。
保存が何度も失敗するなら、捕捉したリクエスト——ヘッダー、ボディ、サーバーの応答——を調べます。十中八九、検証の前に JSON ミドルウェアがボディを静かに書き換えています。
3 秒の締め切り
Discord は 3 秒未満の応答を要求し、超えるとインタラクションは失敗とみなされます。それより長い処理には、まずタイプ 5 の応答(deferred)を送り、後で元のインタラクションを実際の返信で PATCH します。ローカルテストはどのハンドラーが遅すぎるかを明白にします。リクエスト捕捉が正確なレイテンシを示します。
送信 Webhook はテストにまだ役立つ
ローカルコードからチャンネルへメッセージを送りたいだけなら、送信 Webhook URL はトンネルなしで動きます。JSON を POST すればメッセージが表示されます。ローカルの開発実行からのログアラート送信に使っています。
この構成を卒業するとき
少数のコマンドを持つ単一ボットなら、トンネルでのローカルテストで十分です。数百万人にサービスする公開ボットを運用するなら、いずれ安定 URL のステージング配置が欲しくなります。トンネルのセッションがローテーションするたびに Discord が再検証しなくて済むようにするためです。
Discord の 3 秒タイムアウトは GitHub の 10 秒ウィンドウと同じ系統の問題です。詳しくは Webhook の再試行と冪等性をご覧ください。トンネルと捕捉を 1 つの CLI で使うなら PortPreview のウェイトリストへ。