Все статьи
Discordwebhook debugginglocal testingbots

Вебхуки Discord против interactions, тест локально

Discord называет «вебхуками» две вещи, которые ведут себя совершенно по-разному. Одна — это URL по принципу «отправил и забыл» для публикации сообщений. Другая — endpoint interactions, который получает подписанные POST-запросы всякий раз, когда пользователь вызывает slash-команду. Первая работает на localhost. Вторая — никогда. Это руководство о второй, потому что именно ей нужен туннель.

Две разновидности вебхуков Discord

Исходящие вебхуки — это URL, на которые вы делаете POST. Вы отправляете JSON-сообщение, оно появляется в канале. Туннель не нужен, потому что трафик идёт с вашей машины наружу.

Endpoint-ы interactions — обратное направление. Вы регистрируете URL в Discord; Discord делает POST на него, когда пользователь вызывает /вашу-команду. Endpoint должен быть HTTPS, отвечать в течение трёх секунд и проверять подпись Ed25519 на каждом запросе. Localhost сам по себе ничего из этого не умеет.

Почему endpoint-ы interactions строже большинства

Discord хочет не просто HTTPS — он требует доказать, что вы контролируете endpoint, прежде чем принять URL. Проверка происходит при регистрации: Discord шлёт interaction PING, ваш сервер должен ответить подписанным PONG. Если проверка Ed25519 не пройдёт хоть раз, Discord откажется сохранять URL.

С первого раза часть с Ed25519 вы не настроите. Почти ни у кого не получается.

Проверка Ed25519 — та самая болезненная часть

Discord использует Ed25519 (не HMAC SHA-256) и присылает два заголовка на каждой interaction:

  • X-Signature-Ed25519 — подпись в hex
  • X-Signature-Timestamp — временная метка в секундах

Вы подписываете timestamp + rawBody публичным ключом, который Discord показывает в портале разработчика. В большинстве языков для этого есть библиотека. В Node:

import nacl from 'tweetnacl';

const valid = nacl.sign.detached.verify(
  Buffer.from(timestamp + rawBody),
  Buffer.from(signature, 'hex'),
  Buffer.from(PUBLIC_KEY, 'hex'),
);

Причина номер один провала проверки — снова — middleware, который парсит тело до того, как выполнится этот код. Вам нужна сырая строка, которую прислал Discord, а не пересобранная версия распарсенного объекта.

Пошагово: endpoint interactions на localhost

  1. Запустите HTTP-сервер вашего бота локально (любой порт).
  2. Выполните npx portpreview 3000, чтобы получить публичный HTTPS-адрес.
  3. В портале разработчика Discord откройте приложение и вставьте URL туннеля вместе с путём interactions в поле Interactions Endpoint URL.
  4. Нажмите Save. Discord сразу пошлёт PING. Если проверка Ed25519 работает, URL сохранится. Если нет, поле покажет ошибку.
  5. После сохранения вызовите slash-команду на любом сервере, где есть ваш бот. Запрос придёт в ваш обработчик.

Если сохранение раз за разом не удаётся, изучите захваченный запрос — заголовки, тело и ответ вашего сервера. В девяти случаях из десяти тело было незаметно изменено JSON-middleware до проверки.

Дедлайн в три секунды

Discord требует ответ менее чем за три секунды, иначе считает interaction провалённой. Для всего, что дольше, отправьте первый ответ типа 5 (deferred), а затем PATCH-ом обновите исходную interaction настоящим ответом позже. Локальное тестирование сразу показывает, какие обработчики слишком медленные — захват запросов отображает точную задержку.

Исходящие вебхуки всё ещё полезны для тестов

Если нужно просто отправлять сообщения в канал из локального кода, URL исходящего вебхука работает без туннеля. POST JSON — сообщение появилось. Мы используем это для алертов из логов локальных прогонов.

Когда эта схема перестаёт хватать

Для одного бота с горсткой команд локального тестирования с туннелем более чем достаточно. Если вы ведёте публичного бота на миллионы пользователей, со временем захочется staging-деплой со стабильным URL, чтобы Discord не перепроверял всё каждый раз при ротации сессии туннеля.

Таймаут Discord в три секунды — та же семья проблем, что и десятисекундное окно GitHub. Прочитайте наш разбор повторов вебхуков и идемпотентности для подробной версии. Запишитесь в лист ожидания PortPreview, чтобы получить туннель и захват в одном CLI.

Часто задаваемые вопросы

Можно ли тестировать slash-команды Discord на localhost?
Напрямую — нет. Discord требует HTTPS, проверку подписи Ed25519 и окно ответа в три секунды. Используйте туннель, чтобы открыть локальный сервер по HTTPS, и укажите его как endpoint interactions в портале разработчика.
Почему Discord отклоняет URL моего endpoint interactions?
При сохранении URL Discord сразу шлёт interaction PING. Если проверка Ed25519 не проходит или обработчик не отвечает корректным PONG, сохранение отклоняется. Чаще всего причина — middleware, парсящий тело запроса до проверки.
Нужен ли исходящим вебхукам Discord туннель?
Нет. Исходящие вебхуки — это URL, на которые вы делаете POST из кода, поэтому трафик идёт только наружу. Туннель нужен только endpoint-ам interactions, которые принимают POST от Discord.