Discord nazywa „webhookami" dwie rzeczy, które zachowują się zupełnie inaczej. Jedna to URL typu fire-and-forget do wysyłania wiadomości. Druga to endpoint interactions, który odbiera podpisane POST-y za każdym razem, gdy użytkownik uruchomi komendę slash. Pierwsza działa na localhost. Druga nigdy. Ten poradnik dotyczy drugiej — bo to ona potrzebuje tunelu.
Dwa rodzaje webhooków Discord
Webhooki wychodzące to URL-e, na które robisz POST. Wysyłasz wiadomość JSON, pojawia się na kanale. Tunel niepotrzebny, bo ruch wychodzi z twojej maszyny na zewnątrz.
Endpointy interactions idą w przeciwnym kierunku. Rejestrujesz URL w Discordzie; Discord robi POST, gdy użytkownik uruchomi /twojakomenda. Endpoint musi być HTTPS, odpowiedzieć w mniej niż trzy sekundy i weryfikować podpis Ed25519 przy każdym żądaniu. Localhost sam tego nie potrafi.
Dlaczego endpointy interactions są surowsze niż większość
Discord chce nie tylko HTTPS — wymaga udowodnienia, że kontrolujesz endpoint, zanim przyjmie URL. Weryfikacja dzieje się przy rejestracji: Discord wysyła interakcję PING, twój serwer musi odpowiedzieć podpisanym PONG. Jeśli weryfikacja Ed25519 zawiedzie choć raz, Discord odmawia zapisania URL.
Części Ed25519 nie ogarniesz za pierwszym razem. Prawie nikomu się nie udaje.
Weryfikacja Ed25519, część która gryzie
Discord używa Ed25519 (nie HMAC SHA-256) i dostarcza dwa nagłówki przy każdej interakcji:
X-Signature-Ed25519— podpis w hexX-Signature-Timestamp— timestamp w sekundach
Podpisujesz timestamp + rawBody kluczem publicznym, który Discord pokazuje w portalu deweloperskim. Większość języków ma do tego bibliotekę. W Node:
import nacl from 'tweetnacl';
const valid = nacl.sign.detached.verify(
Buffer.from(timestamp + rawBody),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex'),
);
Przyczyną numer jeden nieudanej weryfikacji jest — znowu — middleware parsujący ciało, zanim ten kod się wykona. Potrzebujesz surowego ciągu, który wysłał Discord, a nie ponownie zserializowanej wersji sparsowanego obiektu.
Krok po kroku: endpoint interactions na localhost
- Uruchom serwer HTTP swojego bota lokalnie (dowolny port).
- Wykonaj
npx portpreview 3000, by uzyskać publiczny adres HTTPS. - W portalu deweloperskim Discord otwórz aplikację i wklej adres tunelu wraz ze ścieżką interactions w Interactions Endpoint URL.
- Kliknij Save. Discord od razu wyśle
PING. Jeśli weryfikacja Ed25519 działa, URL się zapisze. Jeśli nie, pole pokaże błąd. - Po zapisaniu uruchom komendę slash na dowolnym serwerze, gdzie jest twój bot. Żądanie trafi do twojego handlera.
Jeśli zapis wielokrotnie zawodzi, zbadaj przechwycone żądanie — nagłówki, ciało i odpowiedź serwera. Dziewięć na dziesięć razy ciało zostało po cichu zmienione przez middleware JSON przed weryfikacją.
Termin trzech sekund
Discord wymaga odpowiedzi poniżej trzech sekund, inaczej liczy interakcję jako nieudaną. Dla czegokolwiek dłuższego wyślij wstępną odpowiedź typu 5 (odroczoną), a potem zrób PATCH oryginalnej interakcji z prawdziwą odpowiedzią później. Lokalne testowanie pokazuje wprost, które handlery są zbyt wolne — przechwytywanie żądań podaje dokładne opóźnienie.
Webhooki wychodzące nadal przydają się do testów
Jeśli chcesz tylko wysyłać wiadomości na kanał z lokalnego kodu, URL webhooka wychodzącego działa bez tunelu. POST JSON, wiadomość się pojawia. Używamy tego do alertów z logów lokalnych przebiegów dev.
Kiedy wyrastasz z tej konfiguracji
Dla pojedynczego bota z garstką komend lokalne testowanie z tunelem w zupełności wystarcza. Jeśli utrzymujesz publicznego bota obsługującego miliony użytkowników, w końcu zechcesz wdrożenie staging ze stabilnym URL, by Discord nie musiał weryfikować od nowa przy każdej rotacji sesji tunelu.
Trzysekundowy timeout Discorda to ta sama rodzina problemów co dziesięciosekundowe okno GitHuba. Przeczytaj nasze ujęcie ponawiania webhooków i idempotencji po długą wersję. Dołącz do listy oczekujących PortPreview po tunel + przechwytywanie w jednym CLI.