← Все статьи
Руководство

Я дал Cursor документацию по Webhook — и за полчаса получил Telegram→Slack ретранслятор — UnifyPort

Если вам нужны сообщения из Telegram в Slack, стандартный путь таков: создать бота через @BotFather, настроить Webhook или getUpdates-поллинг, распарсить вложенный объект Update Telegram, отформатировать для Incoming Webhook Slack и обработать лимиты. Это работает, но код привязан исключительно к Telegram. Объект Update имеет собственную структуру, бот видит только свои диалоги, а если позже вы захотите получать WhatsApp-сообщения в тот же Slack-канал — придётся интегрировать совершенно другой API с нуля.

Я пошёл другим путём. Открыл Cursor, вставил справочник Webhook UnifyPort и попросил собрать Telegram→Slack ретранслятор. Через тридцать минут у меня был Express-сервер, который проверяет подпись каждой доставки через HMAC-SHA256, форматирует входящие Telegram-сообщения с помощью Slack Block Kit и отправляет их в канал — примерно 45 строк кода. Тот же ретранслятор обрабатывает WhatsApp, LINE, TikTok, Zalo и X без каких-либо изменений, потому что структура входящего события одинакова для всех шести платформ.

Что получится в итоге

Node.js Express-сервер, который:

  1. Принимает события message.received от унифицированного Webhook UnifyPort
  2. Проверяет подпись каждой доставки через HMAC-SHA256 с вашим signing_secret
  3. Форматирует каждое сообщение в Slack Block Kit с тегом платформы, отправителем и временной меткой
  4. Отправляет в Slack-канал через Incoming Webhook

Время: около 30 минут. Понадобится рабочее пространство UnifyPort с подключённым Telegram-аккаунтом (QR-сканирование — регистрация бота не требуется) и URL Slack Incoming Webhook.

Почему не использовать Bot API напрямую?

Краткий обзор того, что требует путь через Telegram Bot API:

  1. Создать бота через @BotFather — вы получаете bot token, но бот — это отдельная сущность, не ваш личный аккаунт
  2. Выбрать способ доставки: поллинг getUpdates или Webhook — оба требуют настройки, специфичной для Telegram
  3. Распарсить объект Update: текст сообщения в update.message.text, отправитель в update.message.from.id, чат в update.message.chat.id — структура уникальна для Telegram
  4. Обработать лимиты: 30 сообщений/секунду глобально, 20 сообщений/минуту в одну группу
  5. Отформатировать и переслать в Slack

Код работает — пока вам не понадобятся WhatsApp-сообщения в том же канале. Тогда вы интегрируете WhatsApp Cloud API отдельно, парсите другой Webhook-пейлоад и поддерживаете два кодовых пути, оба выдающих Slack-сообщение. Три платформы — три кодовых пути.

Webhook UnifyPort решает это. Каждая платформа доставляет одно и то же событие message.receivedprovider, from, text, message_id. Один обработчик обслуживает все шесть каналов.

Подготовка: передайте документацию в Cursor

Перед написанием промптов загрузите API-справочник в контекст Cursor. Используйте @Docs для добавления справочника Webhook UnifyPort — особенно:

  • Структура пейлоада события message.received
  • Заголовок x-unifyport-signature и описание проверки HMAC-SHA256
  • Вызов создания эндпоинта POST /v1/webhook-endpoints

Или вставьте эти секции прямо в чат. Главное: реальные имена полей на входе — корректный код на выходе. Без справочника Cursor придумает коллбэк telegram.onMessage() или обработчик bot.on('text'), которых в API UnifyPort не существует.

Claude Code, Windsurf и Copilot работают точно так же — файловый контекст, панель документации или вставка в чат. Инструмент вторичен; документация — главное.

Сборка

Первый промпт — скелет ретранслятора:

Используя документацию UnifyPort webhook, напиши Express-сервер с маршрутом POST /webhook. Когда event равен “message.received”, отправляй отформатированное сообщение на Slack incoming webhook по URL из SLACK_WEBHOOK_URL. Включи имя provider, отправителя и текст сообщения. Для всех событий возвращай 200.

Cursor читает справочник и генерирует:

import express from "express";

const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;

const app = express();
app.use(express.json());

app.post("/webhook", async (req, res) => {
  const evt = req.body;
  if (evt.event === "message.received") {
    await fetch(SLACK_WEBHOOK, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        text: `*[${evt.provider}]* ${evt.from}: ${evt.text}`,
      }),
    });
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

Пятнадцать строк обработчика. Поскольку структура события одинакова для всех шести платформ, парсинг, специфичный для Telegram, не нужен — evt.provider содержит "telegram", но структура идентична сообщению из WhatsApp или LINE.

Второй промпт — проверка подписи:

Каждая доставка включает заголовок x-unifyport-signature. Проверяй его через HMAC-SHA256, используя signing_secret из переменной окружения. HMAC должен вычисляться по сырому телу запроса — не по ресериализованному JSON. При неудаче возвращай 401. Используй timing-safe сравнение.

Cursor добавляет слой проверки:

import crypto from "crypto";

const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;

app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }
}));

function isValid(req) {
  const sig = req.get("x-unifyport-signature") || "";
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(req.rawBody)
    .digest("hex");
  return sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Коллбэк verify захватывает сырой буфер до того, как Express распарсит JSON. Если бы Cursor использовал req.body и ресериализовал его, разница в пробелах привела бы к несовпадению HMAC. Промпт указал «сырое тело запроса», поэтому реализация получилась корректной без доработок.

Третий промпт — улучшенное форматирование Slack:

Измени Slack-сообщение на формат Block Kit. Отображай provider как тег, sender ID жирным. Добавь context-блок с timestamp и message_id.

Финальный server.js:

import express from "express";
import crypto from "crypto";

const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;

const app = express();
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }
}));

function isValid(req) {
  const sig = req.get("x-unifyport-signature") || "";
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(req.rawBody)
    .digest("hex");
  return sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

app.post("/webhook", async (req, res) => {
  if (!isValid(req)) return res.sendStatus(401);

  const evt = req.body;
  if (evt.event === "message.received" && SLACK_WEBHOOK) {
    await fetch(SLACK_WEBHOOK, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: `*[${evt.provider}]* from *${evt.from}*\n${evt.text}`,
            },
          },
          {
            type: "context",
            elements: [
              {
                type: "mrkdwn",
                text: `${evt.message_id} · ${new Date(evt.timestamp * 1000).toISOString()}`,
              },
            ],
          },
        ],
      }),
    });
  }
  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

Менее 50 строк. Проверка подписи, форматирование Slack Block Kit, пересылка всех входящих сообщений. Обработчик никогда не проверяет значение provider, потому что в этом нет необходимости.

Запустите и посмотрите, как приходит сообщение из Telegram

Подключите Telegram-аккаунт в рабочем пространстве UnifyPort — Telegram поддерживает QR-аутентификацию, сканируйте из приложения Telegram, и через несколько секунд всё готово. Без @BotFather, без bot token, без управления API-ключами. Подключается ваш реальный Telegram-аккаунт, а не отдельный бот.

Зарегистрируйте Webhook-эндпоинт, указывающий на ваш сервер, с subscribed_events: ["message.received"] и signing_secret. Запустите сервер, затем отправьте тестовое сообщение на ваш Telegram-аккаунт от другого пользователя. Событие приходит:

{
  "event": "message.received",
  "account_id": "acct_5Qm8nR",
  "provider": "telegram",
  "from": "user_7c3d9e",
  "text": "Завтра в три всё ещё встречаемся?",
  "timestamp": 1750521600,
  "message_id": "tg_msg_2a6f4b"
}

Сервер проверяет подпись, форматирует Block Kit пейлоад и отправляет в Slack. Сообщение появляется в канале с тегом платформы, отправителем, временной меткой и полным текстом. Если проверка подписи возвращает 401 — убедитесь, что UNIFYPORT_SIGNING_SECRET совпадает со значением, установленным на Webhook-эндпоинте.

Архитектурный выигрыш: добавьте WhatsApp без изменения кода

Здесь ретранслятор расходится с Bot API-решением. Подключите WhatsApp-аккаунт в том же рабочем пространстве, подпишите тот же Webhook-эндпоинт. WhatsApp-сообщение приходит в точно такой же структуре:

{
  "event": "message.received",
  "account_id": "acct_3Xk1wL",
  "provider": "whatsapp",
  "from": "user_9b4e7a",
  "text": "Счёт отправлен, подтвердите получение",
  "timestamp": 1750521660,
  "message_id": "wa_msg_8d2c5f"
}

Тот же обработчик. Тот же Slack-канал. Тег [whatsapp] отличает его от [telegram] в Slack, но кодовый путь идентичен. Для Telegram Bot API ретранслятора потребовалась бы отдельная интеграция — другой Webhook-пейлоад, другая логика парсинга, другая обработка ошибок. Этот ретранслятор обрабатывает LINE, TikTok, Zalo и X тем же способом, потому что структура события не меняется между платформами.

В этом разница между разработкой под API одной платформы и разработкой под нормализованный слой. Путь через Bot API привязывает ретранслятор к Telegram. Справочник Webhook UnifyPort даёт Cursor — или любому AI-инструменту для написания кода — достаточно простую поверхность, чтобы завершить работу с первой попытки, а результат работает для каждого канала, который вы подключите позже. Загрузите справочник API UnifyPort в ваш инструмент, вставьте схему message.received, и ретранслятор будет работать до обеда.