Wiki / Core Features / Исходящие вебхуки

Исходящие вебхуки

Обновлено Maxime_48 · 1 час назад · Просмотры: 64

Языки: English Français Deutsch Español Português (Brasil) Italiano Nederlands Polski Türkçe Русский 日本語 한국어 中文

Вебхук отправляет активность вашей команды на принадлежащий вам URL в тот же момент, когда она происходит. Каждое действие, попадающее в ваш Журнал аудита, можно отправлять как подписанный JSON-запрос POST на ваш собственный сервер, вашего собственного бота или внутренний инструмент - без опроса, без экспорта, без парсинга панели.

Одно правило определяет всё на этой странице. Вебхук несёт ровно то, что показывает Журнал активности вашей команды, и ничего больше. Не параллельную ленту с собственным охватом, не более богатую нагрузку для машин. Если этого нет на той странице, то этого нет и в теле запроса.

Активный вебхук команды с категориями доставки и временем последней доставки


📍 Где его найти

Настройки команды → Вебхуки, на странице вашей команды. До трёх адресов на команду, у каждого свой URL, свои категории и свой секрет подписи.

Кто может его настроить: владелец команды или участник, чья роль включает Удалять сервер из команды. Это то же правило, которое открывает сам Журнал активности - вебхук пересылает то, что отображает эта страница, поэтому право читать её - это право пересылать. См. Роли и права команды.


🎯 Что отправляется, а что - никогда

Вы выбираете категории. Доставляются только те, которые вы отметили:

Категория Что охватывает
Функции Функция включена, выключена или сохранены её настройки
Серверы Сервер добавлен в команду или удалён из неё
Команда Команда переименована, изменены роли, изменены вебхуки
Участники Участники приглашены, добавлены, удалены или вышли
Модерация Санкции, апелляции, жалобы, тикеты, очищенные истории
Подписка Изменения подписки команды
Оплата События оплаты команды
Действия платформы над этой командой Что администратор платформы сделал с одним из ваших серверов

🚫 Три вещи никогда не передаются

  • IP-адреса. В Журнале активности IP показывается тому, кому он принадлежит, и больше никому. У вебхука нет читателя, с которым можно сверить, поэтому поле не отправляется - оно удаляется из нагрузки, а не затирается.
  • Действия бэкофиса, не касающиеся вас. Администрирование всей платформы не несёт команды, поэтому ничто в рассылке не может до него дотянуться. Категория Действия платформы над этой командой - это только то, что администратор сделал с вашими серверами.
  • Учётные данные. Любое поле, похожее на токен, ключ или секрет, приходит как •••••••• с обеих сторон пары до/после. Это делается ещё раз на выходе, независимо от маскировки, которую выполняет страница.

Входы в аккаунт не предлагаются. В журнале аудита есть категория auth, и она намеренно отсутствует в списке выше: отправка событий входа на сторонний сервер, без присмотра и бессрочно, - это не то же самое, что показывать их на странице, которую кому-то нужно было открыть. Возможно, она будет добавлена позже.


📦 Доставка

Каждая доставка - это один запрос POST с телом в JSON:

{
  "version": 1,
  "delivery_id": "0a4b2f0e-8c6e-4a5f-9b3d-2f6c1e0d7a11",
  "event": "server.feature.enabled",
  "category": "feature",
  "team": { "id": 12, "name": "Night Shift" },
  "data": {
    "id": 918273,
    "action": "server.feature.enabled",
    "category": "feature",
    "description": "Enabled Starboard",
    "actor_name": "Ava",
    "actor_role": "moderator",
    "is_admin_action": false,
    "team_id": 12,
    "team_name": "Night Shift",
    "server_id": 4,
    "server_name": "Night Shift HQ",
    "subject_label": "Starboard",
    "changes": { "threshold": { "before": 3, "after": 5 } },
    "metadata": null,
    "created_at": "2026-09-06T09:41:12+00:00"
  }
}

Эти пятнадцать полей - весь data: те же пятнадцать, которые отображает Журнал активности, за вычетом IP-адреса. Любое из них может быть null: у действия без сервера "server_id": null, у действия, у которого нечего сравнивать, "changes": null. Читайте их осторожно, а не предполагайте форму для каждого события.

  • version - версия конверта. Она нужна, чтобы ваш получатель мог ветвиться по ней, а не гадать, когда форма расширится.
  • event - аудируемое действие. Их больше ста тридцати, и список растёт вместе с платформой - сопоставляйте по префиксу или по category, а не по исчерпывающему списку названий.
  • delivery_id остаётся одним и тем же при всех повторах одного и того же события. Именно это делает возможной дедупликацию; см. ниже.
  • data - сама запись аудита, тот же объект, который отображает Журнал активности.

📨 Заголовки

Заголовок Значение
User-Agent YAWBDB-Webhooks/1.0
X-YAWBDB-Event Название действия, то же, что event в теле
X-YAWBDB-Delivery Идентификатор доставки, тот же, что delivery_id
X-YAWBDB-Timestamp Unix-время в секундах, когда запрос был сформирован
X-YAWBDB-Signature v1= и затем шестнадцатеричный дайджест

🔐 Проверка подписи

Любой, кто узнает URL вашего адреса, может отправить на него POST с чем угодно. Подпись - это то, как вы отличаете наши доставки от чужих.

Дайджест - это HMAC-SHA256 от метки времени, точки и затем сырого тела запроса, с ключом - вашим секретом подписи:

signed_string = X-YAWBDB-Timestamp + "." + raw_body
signature     = "v1=" + hex( hmac_sha256(signed_string, secret) )

Три детали несут нагрузку, и пропуск любой из них оставляет дыру:

  • Подписывайте сырые байты, до любого разбора JSON. Повторная сериализация тела меняет его - порядок ключей, экранированные слэши, юникод - и дайджест не совпадёт.
  • Метка времени находится внутри подписываемой строки, а не просто рядом с ней. Именно это позволяет отклонять повторы: отказывайтесь от доставки, метка времени которой старше нескольких минут (разумное окно - пять минут), и злоумышленник не сможет повторно отправить запрос, перехваченный на прошлой неделе. Поскольку метка времени подписана, изменить её он тоже не может.
  • Сравнивайте за постоянное время. Простое == возвращается на первом отличающемся байте, и этой разницы во времени достаточно, чтобы восстановить подпись по одному байту. Используйте hash_equals, crypto.timingSafeEqual, hmac.compare_digest - как это называется в вашем языке.

PHP

$raw       = file_get_contents('php://input');
$timestamp = (int) ($_SERVER['HTTP_X_YAWBDB_TIMESTAMP'] ?? 0);
$given     = $_SERVER['HTTP_X_YAWBDB_SIGNATURE'] ?? '';

if (abs(time() - $timestamp) > 300) {
    http_response_code(400);   // too old, or clock is wrong
    exit;
}

$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);

if (!hash_equals($expected, $given)) {
    http_response_code(401);
    exit;
}

http_response_code(200);       // accepted

Node.js (Express)

import crypto from 'node:crypto';

// The raw body is required - express.json() alone destroys it.
app.post('/yawbdb', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = Number(req.get('X-YAWBDB-Timestamp'));
  const given     = req.get('X-YAWBDB-Signature') ?? '';

  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return res.sendStatus(400);

  const expected = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${req.body}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(given);

  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  res.sendStatus(200);
});

Префикс v1= называет схему. Если она когда-нибудь изменится, получатель, проверяющий префикс, сможет намеренно отклонить неизвестный, а не молча прочитать его неправильно.

🔑 Секрет

64 шестнадцатеричных символа, генерируются для вас, показываются ровно один раз - в панели, сразу после создания адреса или нажатия Новый секрет. Потом ничто не читает его обратно: ни страница, ни API, ни экспорт. Потеряете - создадите новый.

Смена секрета вступает в силу немедленно для всего, что отправляется с этого момента. Уже поставленные в очередь доставки продолжают уходить, подписанные старым секретом, пока очередь не опустеет, поэтому ожидайте короткого перекрытия и сначала обновите своего получателя.


🔁 Доставка, повторы и дедупликация

  • Получением считается только 2xx. Всё остальное - 3xx, 4xx, 5xx - это неудача. Отвечайте быстро, а работу делайте потом.
  • Перенаправления не выполняются. 302 - это неудача, а не переход. Дайте нам конечный URL.
  • Тайм-ауты короткие: три секунды на соединение, пять секунд на ответ. Это уведомление, а не удалённый вызов процедуры - сначала подтвердите, обрабатывайте потом.
  • Четыре попытки с интервалами 30 секунд, 2 минуты, затем 10 минут: примерно двенадцать минут от начала до конца. Этого хватает на деплой, но не на сбой.
  • Повторы используют тот же delivery_id. Получатель, который считает каждый POST новым, будет считать дважды в тот день, когда наша первая попытка у вас удалась, но у нас завершилась тайм-аутом. Сохраняйте идентификатор и игнорируйте уже обработанный.
  • Порядок не гарантируется. Доставки ставятся в очередь независимо; повторно отправленная приходит после событий, случившихся позже. Если порядок для вас важен, используйте data.created_at.

⛔ Автоматическое отключение

После 20 неудач подряд адрес отключается, и панель сообщает об этом на карточке. Адрес, переставший отвечать, обычно перестал навсегда, и никто не приходит нам об этом сказать - бесконечные повторы стоили бы запрос и строку истории на каждое действие, бессрочно.

Чтобы запустить его снова: исправьте своего получателя, затем нажмите Изменить у вебхука и снова отметьте Доставлять события на этот адрес. Именно повторное включение сбрасывает счётчик, поэтому исправленный адрес не окажется в одной неудаче от нового отключения.


🧪 Отправка теста

Отправить тест сразу отправляет на ваш адрес один настоящий подписанный запрос и показывает код статуса и время полного обращения. Это не симуляция: он проходит через тот же код, через который проходит каждая настоящая доставка, с теми же заголовками, поэтому адрес, прошедший тест, не сможет потом отклонить рабочий трафик из-за заголовка, которого тест не отправлял.

Тело теста содержит "event": "panel.webhook.test" и "category": "test", чтобы ваш получатель мог его распознать.

Ожидайте два поступления, а не одно. Нажатие кнопки само по себе является аудируемым действием команды. Поэтому ваш адрес получает тестовую доставку и - если он подписан на категорию Команда - через мгновение вторую, обычную доставку для team.webhook.tested. Это правильно, и это не цикл.

Тест никогда не учитывается в пределе в 20 неудач и никогда его не сбрасывает. Отладка адреса не может его отключить.


📜 Последние попытки

Последние попытки показывает последние 20 попыток для этого адреса: когда, какое событие, номер попытки, код статуса или ошибку и сколько это заняло. У неудач есть короткий фрагмент того, что ответил ваш сервер, - обычно это самый быстрый способ выяснить, что отказывает обратный прокси, а не ваш код.

Попытки хранятся 30 дней и ежедневно чистятся. Это отладочный выхлоп, а не запись: у самого журнала аудита есть собственное, гораздо более долгое хранение.


🛡️ Куда мы отказываемся отправлять

URL адреса должен указывать в публичный интернет. URL, который разрешается в loopback-адрес, частный диапазон, link-local-адрес или адрес метаданных облака, отклоняется - при сохранении и ещё раз перед каждой отдельной отправкой.

Вторая проверка важна со временем. Имя, указывавшее куда-то публичное в день, когда его ввели, через месяц может указывать на 127.0.0.1, а вебхук срабатывает, пока существует. Отказ считается неудачей адреса и не повторяется: задать резолверу тот же вопрос ещё три раза ничего бы не изменило.

Предпочитайте адрес https. Подпись доказывает, кто отправил тело, но не скрывает его - по обычному http ваша активность идёт открытым текстом.


💬 Прямо в канал Discord или Slack

Вставьте URL вебхука Discord (https://discord.com/api/webhooks/…) в качестве адреса, и панель отправит сообщение Discord вместо JSON-конверта: один эмбед на запись, с описанием в заголовке, с событием, категорией, исполнителем, командой, сервером, объектом и - когда они есть - изменениями в виде короткого diff. Метка времени берётся из записи, а в подвале указан идентификатор доставки.

Эмбед строится из того же конверта, описанного выше, поэтому на него действуют те же два правила: без IP-адреса, учётные данные замаскированы. Упоминания отключены на стороне Discord, поэтому канал или роль с именем @everyone в записи публикуется как текст и никого не пингует. Заголовки X-YAWBDB-* и подпись по-прежнему отправляются; Discord их просто игнорирует.

Discord отвечает 204 No Content на принятое сообщение, и именно это показывает для него зелёная строка в Последних попытках.

То же относится к URL входящего вебхука Slack (https://hooks.slack.com/services/…): панель публикует по одному сообщению Slack на запись - описание жирным, затем по строке на каждый факт, изменения в блоке кода, а ниже метка времени и идентификатор доставки. Оно строится из того же конверта, поэтому действуют те же два правила, а три символа, которые Slack резервирует для собственных команд (<, >, &), экранируются, так что имя, введённое участником, никогда не превратится в @channel или @here. Slack отвечает 200 ok на принятое сообщение. URL workflow и trigger в Slack - это не сообщения: они сохраняют JSON-конверт, который workflow может разобрать своими переменными.


💡 Что с этим строят

  • Публикуйте активность вашей команды в собственный канал Discord или Slack - URL вебхука Discord или Slack работает как есть, а можно поставить между ними собственный получатель, чтобы фильтровать и переформатировать.
  • Зеркалируйте журнал в собственное хранилище логов и храните его дольше окна хранения панели.
  • Вызывайте кого-нибудь, когда действие модерации происходит в нерабочее время.
  • Запускайте сборку, синхронизацию или резервное копирование, когда меняется настройка функции.

🔗 См. также