Discord có hai thứ mà người ta gọi là "webhook" và chúng hành xử hoàn toàn khác nhau. Một là URL kiểu bắn-rồi-quên để đăng tin nhắn. Cái còn lại là endpoint interactions, nhận các POST có chữ ký mỗi khi người dùng chạy một slash command. Cái đầu chạy trên localhost. Cái sau thì không bao giờ. Hướng dẫn này nói về cái sau — vì đó mới là cái cần tunnel.
Hai kiểu webhook Discord
Webhook đi ra là URL mà bạn POST tới. Bạn gửi một tin nhắn JSON, nó hiện trong một kênh. Không cần tunnel vì lưu lượng đi ra từ máy của bạn.
Endpoint interactions đi theo hướng ngược lại. Bạn đăng ký một URL với Discord; Discord POST tới đó mỗi khi người dùng chạy /yourcommand. Endpoint phải là HTTPS, phải phản hồi trong vòng ba giây, và phải xác minh chữ ký Ed25519 trên mỗi yêu cầu. Localhost tự nó không làm được điều nào.
Vì sao endpoint interactions khắt khe hơn phần lớn
Discord không chỉ muốn HTTPS — nó buộc bạn chứng minh mình kiểm soát endpoint trước khi chấp nhận URL. Việc xác minh diễn ra lúc đăng ký: Discord gửi một interaction PING, máy chủ của bạn phải phản hồi bằng một PONG có chữ ký. Nếu xác minh Ed25519 thất bại dù chỉ một lần, Discord từ chối lưu URL.
Bạn sẽ không làm đúng phần Ed25519 ngay lần đầu. Gần như không ai làm được.
Xác minh Ed25519, phần cắn người
Discord dùng Ed25519 (không phải HMAC SHA-256) và cung cấp hai header trên mỗi interaction:
X-Signature-Ed25519— chữ ký dạng hexX-Signature-Timestamp— timestamp tính bằng giây
Bạn ký timestamp + rawBody bằng khóa công khai mà Discord hiển thị trong cổng nhà phát triển. Hầu hết ngôn ngữ đều có thư viện cho việc này. Trong Node:
import nacl from 'tweetnacl';
const valid = nacl.sign.detached.verify(
Buffer.from(timestamp + rawBody),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex'),
);
Nguyên nhân số một của xác minh thất bại là — lại một lần nữa — middleware parse body trước khi đoạn mã này chạy. Bạn cần chuỗi thô mà Discord gửi, không phải bản re-stringify của object đã parse.
Từng bước: endpoint interactions trên localhost
- Khởi chạy máy chủ HTTP của bot cục bộ (cổng bất kỳ).
- Chạy
npx portpreview 3000để có một URL HTTPS công khai. - Trong cổng nhà phát triển Discord, mở ứng dụng và dán URL tunnel cùng đường dẫn interactions vào Interactions Endpoint URL.
- Bấm Save. Discord sẽ bắn ngay một
PING. Nếu xác minh Ed25519 của bạn hoạt động, URL được lưu. Nếu không, ô đó hiện lỗi. - Sau khi lưu, chạy một slash command ở bất kỳ server nào có bot của bạn. Yêu cầu sẽ rơi vào handler của bạn.
Nếu việc lưu thất bại nhiều lần, kiểm tra yêu cầu đã bắt — header, body và phản hồi của máy chủ. Chín trên mười lần, body đã bị một middleware JSON âm thầm biến đổi trước khi xác minh.
Hạn chót ba giây
Discord yêu cầu phản hồi dưới ba giây, nếu không nó tính interaction là thất bại. Với bất cứ thứ gì lâu hơn, hãy gửi một phản hồi ban đầu type-5 (hoãn) rồi PATCH interaction gốc với câu trả lời thật sau đó. Kiểm thử cục bộ làm rõ handler nào quá chậm — việc bắt yêu cầu cho bạn thấy độ trễ chính xác.
Webhook đi ra vẫn hữu ích để kiểm thử
Nếu bạn chỉ muốn gửi tin nhắn tới một kênh từ mã cục bộ, URL webhook đi ra hoạt động mà không cần tunnel. POST JSON, tin nhắn xuất hiện. Chúng tôi dùng nó để gửi cảnh báo log từ các lần chạy dev cục bộ.
Khi nào bạn vượt khỏi thiết lập này
Với một bot đơn lẻ có vài lệnh, kiểm thử cục bộ với tunnel là quá đủ. Nếu bạn duy trì một bot công khai phục vụ hàng triệu người dùng, cuối cùng bạn sẽ muốn một bản triển khai staging với URL ổn định để Discord khỏi phải xác minh lại mỗi lần phiên tunnel xoay vòng.
Timeout ba giây của Discord cùng họ vấn đề với cửa sổ mười giây của GitHub. Đọc góc nhìn của chúng tôi về thử lại webhook và tính idempotent để có bản đầy đủ. Tham gia danh sách chờ của PortPreview để có tunnel + bắt yêu cầu trong một CLI.