# Отклики и заявки из Аишки в Telegram

Готовый получатель вебхуков: сохраняет событие на диск, проверяет подпись Аишки и отправляет уведомление в выбранный Telegram-чат. По умолчанию это новые отклики на заказы, предложения и отклики на вакансии. Для запуска нужны Node.js 22.13+ с `node:sqlite`, отдельный Telegram-бот, точный получатель и ваш сервер с HTTPS. Дополнительных npm-зависимостей нет.

Пока не указаны ваши бот, чат и адрес получателя, подключение не настроено. Скрипт не использует чужие боты и не включает отправку сам. Не передавайте токены, секрет подписи или содержимое `.env` в чат с ИИ.

## 1. Подготовьте бота и получателя

1. Создайте отдельного бота через [@BotFather](https://t.me/BotFather) командой `/newbot`. Для личных уведомлений откройте своего бота и нажмите «Начать». Для команды добавьте его только в согласованную группу и отправьте `/start@имя_бота` в эту группу. Все её участники смогут читать уведомления.
2. Скачайте [готовый ZIP](https://eval.su/docs/examples/telegram.zip), распакуйте и откройте папку `examples/integrations/telegram`. Доступ к GitHub не нужен; исходники, тесты и файл зависимостей включены:

   ```sh
   npm ci
   cp .env.example .env
   chmod 600 .env
   ```

3. В `.env` заполните `TELEGRAM_BOT_TOKEN`. Узнайте доступные адреса:

   ```sh
   npm run chats
   ```

   Команда показывает только название, тип и `chatId` чатов из последних событий вашего бота. Она не отправляет сообщения, не подтверждает обновления через `offset` и не меняет существующий Telegram-вебхук. Если бот уже обслуживает другую интеграцию, команда остановится — создайте отдельного бота. Если список пустой, отправьте `/start` боту и повторите.

4. Скопируйте **точный числовой** `chatId` в `TELEGRAM_CHAT_ID`, включая минус для группы. Не используйте общий канал для приватных откликов. Участники Telegram-чата не получают права на запись в Аишке: ссылки всё равно требуют входа и доступа.

## 2. Сохраните подключение в Аишке

Откройте [Интеграции → Вебхуки](https://eval.su/settings/integrations/#webhooks) под своим аккаунтом и выберите нужный личный или корпоративный профиль.

1. Введите адрес своего получателя, например `https://your-domain.example/webhooks/aishka`. Это ваш сервер, не адрес Telegram Bot API.
2. Выберите «Новый отклик», «Новое предложение», «Отклик на вакансию». Их технические имена:
   - `response.received`;
   - `offer.received`;
   - `job_response.received`.
3. В составе данных выберите «Все доступные данные записи», если нужна ссылка на конкретный диалог и подробности. Режим «Событие и ссылка на запись» тоже поддерживается: уведомление будет содержать идентификатор и статус, а кнопка откроет общий раздел сообщений.
4. Нажмите **«Сохранить вебхук»**. Это ещё не включает отправку. Скопируйте показанный один раз `signingSecret` в `AISHKA_WEBHOOK_SECRET` файла `.env`. Ключ API для этого получателя не нужен.

Доступы подключения: `webhooks:read`, `webhooks:write`, `events:read`, а также `responses:read` для откликов и `offers:read` для предложений. Для других событий нужны соответствующие права чтения. Личные события доступны их участникам; выбор компании не открывает чужую личную переписку. [Актуальный справочник событий](https://eval.su/docs/events.md) описывает поля и права.

## 3. Запустите получателя

После заполнения `.env` проверьте бота и назначение без отправки:

```sh
npm run check
```

Команда выполняет только Telegram `getMe` и `getChat`. Это проверка доступности бота и чата, а не доказательство доставки сообщения или права писать в чат.

Запустите:

```sh
npm start
```

Получатель слушает `127.0.0.1:8787`. Настройте HTTPS-прокси своего домена на **POST `/webhooks/aishka`**, сохраняя исходное тело и заголовки `webhook-*`. Пример для существующего `server` nginx:

```nginx
location = /webhooks/aishka {
    client_max_body_size 2m;
    proxy_pass http://127.0.0.1:8787;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_connect_timeout 2s;
    proxy_read_timeout 9s;
}
```

Не публикуйте папку рецепта, `.env` или `state/` как статические файлы. Запускайте процесс постоянным сервисом с автоматическим перезапуском. `STATE_DIRECTORY` должен указывать на постоянный локальный диск, доступный только пользователю сервиса; временная папка контейнера для очереди не подходит. Сохраняйте резервную копию SQLite средствами SQLite, не копируйте один `.sqlite` без WAL во время работы. Один экземпляр процесса обслуживает одну очередь и одного Telegram-получателя.

## 4. Проверьте настоящую доставку

В Аишке нажмите **«Подключить и проверить»**, проверьте точный HTTPS-адрес и подтвердите отправку. Подписанное событие `webhook.test` даст HTTP 200 только после ответа Telegram `ok: true` с `message_id`.

1. В Telegram должно появиться сообщение «Аишка: подключение работает» с UUID события.
2. В Аишке обновите статус: подключение должно стать активным, история теста — успешной.
3. На принимающем сервере выполните:

   ```sh
   npm run status
   ```

   Для теста должны быть `state: delivered`, `code: telegram_accepted`, `telegramMessageId`. Это подтверждение приёма Telegram, а не прочтения человеком.
4. Создайте настоящий отклик разрешённым способом из второго тестового аккаунта. Сверьте UUID события и диалог. Повторная доставка того же UUID не должна создавать ещё одно уведомление.

В этом рецепте обычный HTTP 202 означает **надёжное сохранение в локальную очередь**. Успех вебхука в Аишке для обычного события ещё не означает доставку Telegram: итог смотрите через `npm run status` и в самом чате. Файлы, личные контакты, резюме и чужая переписка целиком не пересылаются.

## Что отправляется

По умолчанию: вид события, доступный статус, UUID записи и события, кнопка перехода в Аишку. Ни API-ключ, ни секрет подписи в сообщение не попадают. Состав текста ограничен разрешёнными полями; произвольный JSON события не отправляется.

Чтобы добавить текст отклика, бюджет и срок, установите `INCLUDE_TEXT=1` **только для согласованного получателя** и перезапустите сервис. Выберите состав данных `current` в Аишке. Ограниченный текст берётся из доступного `data.resource.payload.body` или `data.resource.body`; бюджет и срок — из `payload`. Имена, цены, сроки и результаты скрипт не выдумывает. Удалённая или недоступная запись не раскрывается. Ссылки на файлы не скачиваются и не обходят проверку доступа.

Для других сценариев измените `AISHKA_EVENTS` и тот же набор событий в настройках Аишки. Например:

```dotenv
AISHKA_EVENTS=message.received,deal.started,deal.status_changed
```

Для такого примера нужны `messages:read` и `deals:read`. Все поддержанные события перечислены в `EVENT_LABELS` в `receiver.mjs`; неизвестное значение конфигурации считается ошибкой. Подписанные события, на которые получатель не подписан, игнорируются. После изменения адреса, состава данных или событий Аишка требует нового подтверждения и теста.

## Повторы и восстановление

- Подпись проверяется HMAC-SHA256 по `webhook-id.webhook-timestamp.rawBody`, сравнением без зависимости времени от значения; допустимое расхождение часов — 5 минут. Настройте время сервера. Секрет должен оставаться на сервере. Ротация Аишки поддерживается: одна из подписей запроса должна совпасть с установленным секретом.
- UUID события — постоянный уникальный ключ SQLite. Вставка фиксируется с `synchronous=FULL` до HTTP 202. Повтор текущего состояния записи с тем же UUID не повторяет действие и не перезаписывает сохранённое уведомление. Очередь привязана к домену Аишки, боту и точному чату; другой получатель требует отдельной папки состояния.
- При подтверждённом отказе Telegram `429` или `5xx` с `ok: false` обработчик повторяет попытку с задержкой; учитывает `retry_after`, максимум 20 попыток. `400`, `401`, `403` сохраняются как `failed`; исправьте конфигурацию или доступ бота.
- У Telegram `sendMessage` нет ключа идемпотентности. Если ответ потерян, неверно сформирован или процесс остановился во время отправки, состояние — **`uncertain`**. Автоматическая повторная отправка остановлена: иначе появятся дубли. Сначала найдите сообщение в Telegram по UUID события. Не считайте `uncertain` доказательством ни доставки, ни её отсутствия.

Если сообщение действительно отсутствует, остановите сервис и разрешите повтор вручную:

```sh
node --env-file=.env receiver.mjs --retry EVENT_UUID --confirm-not-delivered
```

Если нашли его в Telegram, остановите сервис и отметьте подтверждение без повторной отправки:

```sh
node --env-file=.env receiver.mjs --ack EVENT_UUID --confirm-delivered
```

Замените `EVENT_UUID` точным UUID из `npm run status`, затем снова запустите сервис. Уже доставленное событие повторить этой командой нельзя. Чтение статуса работает при запущенном сервисе; изменение очереди требует остановленного процесса. Зависший `receiver.lock` можно убрать только после проверки отсутствия его процесса; обычный перезапуск сам распознаёт завершившийся PID.

Содержимое доставленных уведомлений и код результата стираются из очереди через 7 дней. Минимальный UUID и состояние сохраняются для защиты от повторов. Ожидающие сообщения, ошибки `failed` и неопределённые `uncertain` остаются до решения, чтобы их можно было восстановить после исправления причины, даже спустя неделю. Текст Telegram уже отправлен получателю: очистка локальной очереди не удаляет его из Telegram. Приложение не пишет токены, текст сообщений или ответы Telegram в журналы. Ограничьте внешние журналы процесса и nginx одной неделей отдельно.

## Проверка рецепта без настоящего бота

```sh
npm test
```

Проверки используют временную SQLite, настоящий локальный HTTP-получатель и имитатор Telegram. Они проверяют подпись, неизменность байтов, отказ просроченной подписи, сохранение до ответа, повтор после перезапуска, тест с реальным контрактом `webhook.test`, `429`, неоднозначную отправку, ручное восстановление, доступы к файлам состояния и очистку содержимого. Внешние сообщения не отправляются. Это проверка реализации, а не свидетельство подключения вашего Telegram-чата.

Контракт Telegram сверяется с официальными [sendMessage](https://core.telegram.org/bots/api#sendmessage), [getUpdates](https://core.telegram.org/bots/api#getupdates) и [ResponseParameters](https://core.telegram.org/bots/api#responseparameters). Для текста используется обычный `text` без `parse_mode`, поэтому разметка из отклика не исполняется как Telegram HTML/Markdown; предпросмотр ссылок отключён.
