# Отмены и опоздания рейсов из @avp2Polotsk Первый этап читает публичный канал через **отдельный существующий Telegram-аккаунт** и MTProto-клиент `teleproto`. Права управления каналом и Bot API не нужны. Получение выполняется раз в минуту; просматриваются публикации за текущий и два предыдущих дня, включая правки. Вложения не скачиваются. Расписания рейсов не изменяются: импорт создаёт предупреждения существующих типов `cancellation` (отмена) и `delay` (опоздание). ## Распознавание и срок действия Формат «Не будут выполняться рейсы: ПО МАРШРУТУ № 27 С ОСТАНОВКИ АВ/ВОКЗАЛ НА 15.43,16.43. С ОСТАНОВКИ ЗОДЧЕГО НА 15.58,16.58.» разбирается в четыре конкретных отправления. `АВ/ВОКЗАЛ` означает **Автовокзал**, точки в часах заменяются двоеточием. Отмена всего маршрута из такого объявления не выводится. Поддерживаются формулировки **«С ОСТАНОВКИ»**, **«ОТ ОСТАНОВКИ»** и **«С/ОТ ОСТАНОВОЧНОГО ПУНКТА»**, включая их сочетание в одном сообщении. После общего заголовка можно перечислять остановки в кавычках на отдельных строках, например `«Мариненко» на 6.00, 7.30` и `«Боровуха» на 6.40, 8.15`. Заголовок может отделяться от списка пустыми строками. Значения вроде `на 7.15` и `на 8.00` после остановки считаются временем отправления. Если отдельная дата не указана, день отмены берётся из оригинальной публикации. После обновления backend ранее сохранённые сообщения с ошибкой «Некорректная дата отмены» будут обработаны повторно при очередном чтении канала в пределах трёхдневного окна; сообщения с неизвестными остановками или номерами маршрутов останутся на проверке с уточнённой причиной. Номер маршрута относится ко всем его действующим направлениям в выбранном городе. Один пост создаёт одно уведомление, в том числе при нескольких номерах. Поддерживаются отдельные блоки «по маршруту №…» и общий список «по маршрутам №2, №27» с явно указанными остановками и временами. Неизвестные номера и остановки, разные даты, условия, периоды и изображения без текста требуют проверки администратором. Неизвестный номер блокирует автоматическую публикацию всего поста. Обычные новости не импортируются. Пока нет OCR, распознавания произвольных формулировок и автоматического редактирования расписаний. Явная дата, «сегодня» или «завтра» определяют день отмены. Если дата отсутствует, используется **день оригинальной публикации по Europe/Minsk**, а не день импорта или правки. Дата без года выбирается ближайшей к публикации. Конец действия — **00:00 следующего дня по Минску**, граница исключительная. Например, для 02.10.2026 это `2026-10-02T21:00:00Z`. Будущие отмены активируются в начале указанного дня и получают push тогда же. Просроченные объявления сохраняются как завершённые без push. Новые однозначные отмены публикуются автоматически. `TELEGRAM_AUTO_PUBLISH=false` переводит все распознанные отмены на проверку. В админке на странице **«Уведомления» → «Уведомления из Telegram»** доступны состояние подключения, ручная проверка канала, исходные сообщения, ссылки на оригинал и история. При ручной публикации оператор выбирает один или несколько номеров маршрутов, проверяет текст и день отмены; срок до полуночи назначается сервером. ### Словарь остановок и текст уведомления В блоке **«Уведомления из Telegram» → «Словарь названий остановок»** можно добавлять, изменять и удалять соответствия: название из канала → конкретная остановка из каталога выбранного города. Например, `ЗОДЧЕГО` → `Зодчего Иоанна`. Для разных сокращений одной остановки создайте несколько записей. Регистр, пробелы, знаки препинания и различие `ё/е` не учитываются; два эквивалентных сокращения в одном городе запрещены. Если есть несколько платформ с одним названием, выберите нужную остановку по адресу/ID. Отключённая остановка требует исправления соответствия и ручной проверки. Миграция **014** добавляет словарь и начальные соответствия `АВ/ВОКЗАЛ` → `Автовокзал`, `ЗОДЧЕГО` → `Зодчего Иоанна` только для Полоцка и только при единственной действующей остановке с таким названием. При неоднозначности соответствие задаёт администратор. Уже сохранённые соответствия при повторном запуске миграции не перезаписываются. Изменения словаря учитываются при следующем чтении канала, включая сообщения **на проверке с неизменившимся исходным текстом** в трёхдневном окне. Уже опубликованные или отклонённые сообщения повторно из-за изменения словаря не публикуются. Для распознанных отмен backend формирует лаконичный текст без приветствия и подписи перевозчика. Например: ```text Отмена отдельных рейсов №27 02.10.2026: не выполняются рейсы: Автовокзал — 15:43, 16:43 Зодчего Иоанна — 15:58, 16:58 ``` При нескольких номерах создаётся одно уведомление с блоком отправлений для каждого номера. Исходный текст остаётся в истории импорта. В ручной проверке можно выбрать тип «Отмена рейсов» или «Опоздание рейсов». Распознанный текст также подставляется в удобочитаемом виде; выбор номеров подставляет соответствующие отправления, пока оператор не изменил текст самостоятельно. Если сообщение не удалось разобрать, текст для ручной правки остаётся исходным. ## Установка Для существующей базы применить миграцию **013** один раз, до запуска новой версии backend. Она также переводит UTC-значения времени существующих алертов из `timestamp` в `timestamptz`. Это соответствует принятому формату хранения UTC в админке. При наличии вручную внесённых локальных времен сначала проверить их и сделать резервную копию базы. ```bash docker compose exec -T postgres psql -U transit_admin -d polotsk_transit \ -f /docker-entrypoint-initdb.d/013_telegram_alert_import.sql docker compose exec -T postgres \ psql -v ON_ERROR_STOP=1 --single-transaction -U transit_admin -d polotsk_transit \ < backend/migrations/014_telegram_stop_aliases.sql docker compose exec -T postgres \ psql -v ON_ERROR_STOP=1 -U transit_admin -d polotsk_transit \ < backend/migrations/015_alert_route_numbers.sql ``` Если миграции 013 и 014 уже установлены, примените только **015** и пересоберите backend/frontend. На чистой базе Docker применяет все миграции автоматически. Миграция 015 заполняет номера у существующих уведомлений. Опубликованные Telegram-записи сразу не переписываются: при следующей правке/ручной проверке старые уведомления по направлениям объединяются, уже доставленные push повторно не отправляются. **Не запускайте `npm run migrate` на существующей базе**: старые init-миграции проекта не поддерживают повторный запуск. ### Авторизация аккаунта 1. Создать API-приложение на [my.telegram.org](https://my.telegram.org) и получить `api_id` и `api_hash`. 2. На компьютере/сервере оператора создать закрытый каталог **вне checkout**. Пример для локального запуска: ```bash mkdir -p "$HOME/.config/polotsk-transit-telegram" chmod 700 "$HOME/.config/polotsk-transit-telegram" ``` 3. В `backend/.env` указать: ```dotenv TELEGRAM_IMPORT_ENABLED=false TELEGRAM_API_ID=your_api_id TELEGRAM_API_HASH=your_api_hash TELEGRAM_SESSION_FILE=/absolute/private/directory/account.session TELEGRAM_CHANNEL=avp2Polotsk TELEGRAM_CITY_SLUG=polotsk TELEGRAM_AUTO_PUBLISH=true TELEGRAM_POLL_INTERVAL_MS=60000 ``` 4. Из каталога `backend` выполнить `npm ci`, затем `npm run telegram:login`. Номер, код и пароль вводятся оператором локально. Код и пароль скрыты; session string не печатается. Утилита сохраняет файл с правами `600` и работает только с существующим пользовательским аккаунтом. Если Telegram требует регистрацию/дополнительный неподдерживаемый шаг, завершить его в официальном клиенте и повторить авторизацию. 5. Включить `TELEGRAM_IMPORT_ENABLED=true` и перезапустить backend. Для push также должны быть настроены Firebase и `FCM_ENABLED=true`. Файл сессии даёт доступ к аккаунту. Его нельзя коммитить, передавать frontend или присылать в чат. Backend не выполняет интерактивную авторизацию; отсутствующая/отозванная сессия отображается как ошибка подключения. В статус и логи не попадают API hash, сессия, телефон, коды или сырые ошибки клиента. ### Docker / Proxmox Авторизацию выполнить на доверенном хосте. Поместить созданный `account.session` в закрытый каталог на Docker-хосте, например `/opt/polotsk-transit/telegram-secrets`, с правами `600`. В корневом `.env` настроить: ```dotenv TELEGRAM_SECRETS_DIR=/opt/polotsk-transit/telegram-secrets TELEGRAM_SESSION_FILE=/run/telegram-secrets/account.session TELEGRAM_API_ID=your_api_id TELEGRAM_API_HASH=your_api_hash TELEGRAM_IMPORT_ENABLED=true TELEGRAM_AUTO_PUBLISH=true ``` Compose монтирует каталог только для чтения, отдельно от Firebase. После миграции выполнить `docker compose up -d --build backend frontend`. ## Повторы, правки и доставка Идентичность публикации: город + числовой ID канала + ID сообщения. Снимок текста, результат распознавания и связи с алертами сохраняются одной транзакцией; повторный импорт и одновременные читатели не создают второй алерт. Более старая правка не заменяет свежую. Администратор публикует только проверенную версию по `content_hash`; изменившийся текст даёт `409` и требует нового просмотра. Правка публикации обновляет связанные алерты. Если новая версия неоднозначна или больше не распознаётся как отмена/опоздание, прежние алерты выключаются и сообщение возвращается на проверку. Отклонение сохраняется при последующих правках. Удалённый администратором алерт не воссоздаётся при правке исходного сообщения. Автоудаление сообщения в Telegram не отменяет предупреждение раньше полуночи; удаление не трактуется как восстановление движения. Импорт помечает алерты как `notification_pending` в той же транзакции. Существующий scheduler отправляет их после начала действия, использует блокировки и до пяти попыток; Firebase отключён — ожидание сохраняется. Уже доставленный push при правке того же алерта не отправляется повторно. Остаётся обычное окно между приёмом FCM и записью результата в БД: при аварии в этот момент возможен повтор доставки. `alertId` позволяет клиенту показывать один экземпляр уведомления. FCM TTL ограничен остатком срока действия алерта; истёкший алерт не отправляется. Backend передаёт абсолютный `endTime`. **Удаление уже показанного уведомления из системной шторки требует поддержки Android-клиента**: проверить `endTime` при получении, не показывать просроченный push, поставить `setTimeoutAfter(endTime-now)` и исключать истёкшие алерты из локального списка. Изменения Android-клиента не входят в этот этап backend/админки. Правки и отключение алерта не отзывают уже показанный push. При FloodWait worker выдерживает паузу, не запускает параллельные циклы и отображает время следующей попытки. За один цикл лимит составляет 1000 публикаций; превышение отображается как ошибка, а не как успешное чтение всей истории. Сообщения, исчезнувшие из Telegram во время длительного простоя сервера, восстановить нельзя. ## Несколько маршрутов в API Ручные `POST /api/v1/alerts` и `PUT /api/v1/alerts/:id` принимают `route_numbers: ["2", "27"]`. Пустой список создаёт общее городское уведомление; при публикации отмены из Telegram хотя бы один номер обязателен. Сервер проверяет существование действующих маршрутов выбранного города; неизвестный номер отклоняет весь запрос. Повторный выбор и порядок номеров не влияют на идемпотентность. Ответы API и sync содержат `route_numbers` и `route_ids` (ID всех действующих направлений выбранных номеров). Фильтр `GET /alerts/active?route_id=…` и `GET /alerts?route_id=…` возвращает уведомление для любого направления выбранного номера. Старое поле `route_id` сохранено для запросов и ответа с одним номером; при нескольких номерах оно равно `null`. В FCM добавлены строки `routeNumbers` и `routeIds` с JSON-массивами; отправляется одно событие на уведомление. Для корректной привязки нескольких маршрутов в мобильном клиенте необходимо читать эти массивы; Android-код в этом репозитории отсутствует. ## API и проверка Все endpoint требуют JWT администратора и учитывают `X-City-Id`: - `GET /api/v1/telegram-import` — безопасный статус и до 100 сообщений, сначала требующие проверки. - `POST /api/v1/telegram-import/sync` — чтение настроенного канала; запрещено в другом городе. - `POST /api/v1/telegram-import/:id/review` — `content_hash`, `action: publish|dismiss`; для публикации также `service_date`, `route_numbers: ["2", "27"]`, `title`, `message`; необязательное `alert_type: cancellation|delay` (по умолчанию распознанный тип); старое поле `route_id` также принимается. - `GET /api/v1/telegram-import/stop-aliases` — словарь выбранного города. - `POST /api/v1/telegram-import/stop-aliases` — `alias`, `stop_id`; добавить соответствие. - `PUT /api/v1/telegram-import/stop-aliases/:id` — `alias`, `stop_id`; изменить соответствие. - `DELETE /api/v1/telegram-import/stop-aliases/:id` — удалить соответствие. Обычные unit-тесты: `cd backend && npm test`. SQL-тесты используют временный PostgreSQL runtime PGlite и не обращаются к рабочей базе: ```bash npm install --prefix /tmp/polotsk-telegram-test-runtime @electric-sql/pglite --ignore-scripts TELEGRAM_TEST_PGLITE_MODULE=/tmp/polotsk-telegram-test-runtime/node_modules/@electric-sql/pglite \ node --test test/telegramImport.integration.test.js ``` Реальное чтение Telegram и доставка на устройство проверяются после локальной авторизации аккаунта, применения миграции и включения Firebase. ## Опоздания и обновление распознавания Сообщения «будут выполняться рейсы с опозданием/с задержкой» распознаются как `delay`. Формулировки «могут выполняться/прибыть/прийти с опозданием» сохраняют неопределённость в тексте: «возможны опоздания рейсов». Время после названия остановки — время соответствующего рейса, а не обещанное время прибытия. Длительность опоздания автоматически не придумывается. Если в исходном тексте указана величина задержки, сообщение отправляется на ручную проверку, чтобы она не потерялась. Сообщения с отменами и опозданиями одновременно также требуют проверки. Пример автоматически подготовленного уведомления: ```text Задержка отдельных рейсов №4 08.10.2026: с опозданием выполняются рейсы: Мариненко — 19:05 Боровуха — 20:00 ``` Срок действия для опозданий совпадает с отменами: до 00:00 следующего дня по Минску. Истёкшие сообщения не создают новые алерты и не попадают в push-рассылку. Правка источника с отмены на опоздание обновляет тип существующего алерта; уже отправленный push не повторяется. Для этого обновления новые миграции не нужны, если ранее применены 013–015. Пересоберите `backend frontend`: `docker compose up -d --build backend frontend`. При следующем чтении канала сообщения на проверке будут пересчитаны в пределах трёхдневного окна, даже без правки исходного текста. Сообщения за 8 октября при повторной обработке 9 октября получат статус «День уведомления завершён» и не вызовут новую рассылку. Уже опубликованные и отклонённые сообщения с неизменившимся исходным текстом не переписываются. Неизвестные названия остановок по-прежнему требуют пополнения словаря.