Files
ridebus-admin/docs/telegram-cancellation-import.md
Zakhar Timoshenko 249c338c2a feat: enhance Telegram import functionality to support delays and cancellations
- Updated telegramCancellationParser to include parsing for delay notices.
- Added new test cases for handling delay messages and stop-point cancellations.
- Modified telegramImport integration tests to validate new delay handling.
- Enhanced documentation to reflect the addition of delay notifications.
- Updated TelegramImportPanel component to accommodate new alert types.
- Introduced new fixture files for testing delay and cancellation messages.
2026-10-09 02:59:52 +03:00

24 KiB
Raw Permalink Blame History

Отмены и опоздания рейсов из @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 формирует лаконичный текст без приветствия и подписи перевозчика. Например:

Отмена отдельных рейсов №27

02.10.2026: не выполняются рейсы:
Автовокзал — 15:43, 16:43
Зодчего Иоанна — 15:58, 16:58

При нескольких номерах создаётся одно уведомление с блоком отправлений для каждого номера. Исходный текст остаётся в истории импорта. В ручной проверке можно выбрать тип «Отмена рейсов» или «Опоздание рейсов». Распознанный текст также подставляется в удобочитаемом виде; выбор номеров подставляет соответствующие отправления, пока оператор не изменил текст самостоятельно. Если сообщение не удалось разобрать, текст для ручной правки остаётся исходным.

Установка

Для существующей базы применить миграцию 013 один раз, до запуска новой версии backend. Она также переводит UTC-значения времени существующих алертов из timestamp в timestamptz. Это соответствует принятому формату хранения UTC в админке. При наличии вручную внесённых локальных времен сначала проверить их и сделать резервную копию базы.

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 и получить api_id и api_hash.

  2. На компьютере/сервере оператора создать закрытый каталог вне checkout. Пример для локального запуска:

    mkdir -p "$HOME/.config/polotsk-transit-telegram"
    chmod 700 "$HOME/.config/polotsk-transit-telegram"
    
  3. В backend/.env указать:

    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 настроить:

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 и не обращаются к рабочей базе:

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. Формулировки «могут выполняться/прибыть/прийти с опозданием» сохраняют неопределённость в тексте: «возможны опоздания рейсов». Время после названия остановки — время соответствующего рейса, а не обещанное время прибытия. Длительность опоздания автоматически не придумывается. Если в исходном тексте указана величина задержки, сообщение отправляется на ручную проверку, чтобы она не потерялась. Сообщения с отменами и опозданиями одновременно также требуют проверки.

Пример автоматически подготовленного уведомления:

Задержка отдельных рейсов №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 октября получат статус «День уведомления завершён» и не вызовут новую рассылку. Уже опубликованные и отклонённые сообщения с неизменившимся исходным текстом не переписываются. Неизвестные названия остановок по-прежнему требуют пополнения словаря.