feat: Implement Telegram alerts for catalog updates and failures

This commit is contained in:
2026-10-20 23:14:32 +03:00
parent a95784df93
commit e28d65310d
10 changed files with 541 additions and 43 deletions

View File

@@ -49,7 +49,34 @@ Vite работает на http://127.0.0.1:5173 и проксирует `/api`
**Обновления у студента.** При запуске и при возвращении в приложение, если прошло больше часа, клиент сверяет сохранённое расписание с каталогом. Тот же файл с новой датой тихо перечитывается. Так же один раз перечитывается расписание, сохранённое более старым парсером, чем на сервере: `/api/catalog` сообщает `parserVersion`, и PWA сравнивает его с версией сохранённой копии. Поэтому исправления разбора доходят до студентов без перепубликации документа. У сервера без `parserVersion` перечитывания нет. Если для группы появился более новый документ (следующий семестр или год), показывается предложение его открыть; отказ запоминается. Если документ исчез из каталога, приложение предлагает найти группу заново.
Каталог и папка года перечитываются раз в 15 минут, таблица недель и звонки — раз в 6 часов, распознанный документ — при изменении даты файла или версии парсера. Устаревшая копия отвечает сразу, а перечитывание идёт в фоне. Ждать приходится только при первом запуске, в начале нового учебного года и при «Обновить». «Обновить» (`?refresh=1`) принудительно перечитывает всё, но обращается к университету не чаще раза в 2 минуты (таблицу недель — раз в 10 минут), иначе отвечает только что прочитанной копией. После сбоя источника фоновые попытки тоже ждут 2 минуты. Если источник недоступен, используются сохранённые копии с явной пометкой. Фоновая синхронизация при закрытом приложении не реализована.
**Проверка каталога по ETag.** Сервер сам проверяет каталог университета раз в 5 минут, а не только когда приходит запрос. У каждой папки Nextcloud есть ETag, и он меняется, когда меняется что‑то внутри. Сервер хранит листинги прошлого обхода (`.data/catalog-tree.json`) и заново запрашивает только папки с новым ETag. Проверка неизменного каталога — 2 запроса вместо ~100: корень и папка года. Перезалитый документ стоит ещё пары запросов по пути к нему. Раз в 6 часов на всякий случай идёт полный обход. Индекс пересобирается, только если изменился какой‑то документ, поэтому ETag расписаний и кэши клиентов между проверками не сбрасываются. В `/api/health` поле `catalog.lastCrawl` показывает последний обход: число запросов, полный ли он, сколько папок взято из кэша. Поле `watch` показывает время и ошибку последней проверки.
**Оповещения в Telegram.** Если заданы `TELEGRAM_BOT_TOKEN` и `TELEGRAM_CHAT_ID`, сервер пишет владельцу после проверок каталога:
- появился новый документ — с его группами и подозрительными ячейками;
- документ не читается;
- документ перезалит, и в нём появились подозрительные ячейки, которых раньше не было;
- каталог перешёл на новый учебный год, или папка нового года задерживается;
- университет не отвечает дольше часа, а потом снова отвечает.
Первая проверка только сообщает, что оповещения включены, и называет число документов и подозрительных ячеек. Если сообщение не ушло, оно повторится при следующей проверке. С `PSU_PUBLIC_URL` в сообщениях есть ссылка на `/api/diagnostics` документа.
Настройка:
1. Создайте бота у @BotFather и получите токен.
2. Напишите боту любое сообщение.
3. Возьмите `chat.id` из ответа `https://api.telegram.org/bot<токен>/getUpdates`.
4. Положите значения в `.env` рядом с `docker-compose.yml` (файл в `.gitignore`):
```sh
TELEGRAM_BOT_TOKEN=123456:ABC...
TELEGRAM_CHAT_ID=123456789
PSU_PUBLIC_URL=https://psu.rumblur.by
```
5. Перезапустите контейнер: `docker compose up -d`.
Состояние оповещений — `telegram` в `/api/health`: включены ли, сколько отправлено и последняя ошибка. Токен в журналы и в `/api/health` не попадает.
Каталог перечитывается раз в 5 минут (по ETag), папка года — раз в 15 минут, таблица недель и звонки — раз в 6 часов, распознанный документ — при изменении даты файла или версии парсера. Устаревшая копия отвечает сразу, а перечитывание идёт в фоне. Ждать приходится только при первом запуске, в начале нового учебного года и при «Обновить». «Обновить» (`?refresh=1`) принудительно перечитывает всё, но обращается к университету не чаще раза в 2 минуты (таблицу недель — раз в 10 минут), иначе отвечает только что прочитанной копией. После сбоя источника фоновые попытки тоже ждут 2 минуты. Если источник недоступен, используются сохранённые копии с явной пометкой. Фоновая синхронизация при закрытом приложении не реализована.
Ответы API сжимаются (gzip или brotli; семестр группы — около 200 КБ JSON, в gzip — около 5 КБ) и отдаются с `Cache-Control: no-cache` и `ETag`. Клиент хранит ответ, но каждый раз спрашивает заново. Если ничего не изменилось, приходит пустой `304`. Android-клиент делает это сам через дисковый кэш OkHttp. Импорт Word ограничен: два разбора одновременно, ещё четыре ждут, остальным — `429`; с одного адреса — до 10 файлов за 10 минут. За прокси (Caddy, туннель) адрес клиента берётся из `X-Forwarded-For`, если прокси находится в локальной или частной сети. Для `/api/timetable` ETag считается до любой работы — из версии документа, парсера и сервера, таблицы недель, параметров запроса и даты, — поэтому неизменённое расписание отвечает `304` примерно за миллисекунду, не открывая документ.