Бот для сербского школьного дневника: от идеи до полного контроля над оценками
КодингПроблемы сербской школы. Оценки, пропуски и замечания учителя ставят в государственный электронный дневник esDnevnik. Чтобы узнать новости, нужно зайти на сайт, найти ленту событий и перевести записи учителей с сербского.
Нужно было получать каждое изменение в Telegram сразу, как оно появилось. В феврале 2026 года я начал делать бота. Сейчас он проверяет дневник каждые 15 минут, присылает новые оценки и замечания, переводит их на русский или английский и принимает оплату в Telegram Stars.
По дороге я упёрся в блокировку облачных серверов, поднял VPN-туннель через домашний роутер, дважды ловил молчание бота и один раз — спам в первый день учебного года.
Проверил, что задача решаема и бесплатна
1. Идея: можно ли это вообще сделать
Я начал с вопроса: можно ли написать бота, который сам ходит в дневник и присылает изменения. Сразу задал три условия.
- Бот присылает все данные — каждое изменение, а не только оценки.
- Бот для всех родителей: ввёл логин и пароль от дневника — и пользуешься.
- Всё должно стоить как можно меньше.
Сначала я поискал, делал ли кто-то такое. Нашёл на GitHub открытое мобильное приложение для esDnevnik — значит, к данным можно достучаться программно. Нашёл и хорватский e-dnevnik-bot: он делает почти то же самое для хорватского дневника. Это подтвердило, что идея рабочая.
По деньгам выбрал Oracle Cloud Free Tier. Это пожизненно бесплатная виртуальная машина во Франкфурте. Telegram Bot API тоже бесплатный. Расчёт был на ноль рублей в месяц.
Перед тем как писать код, я составил план и постарался подготовиться к трудностям заранее.
2. Разведка: как устроен дневник изнутри
Наугад писать парсер я не стал. Сначала сделал разведку: скрипт на Playwright открывал браузер, логинился в дневник, проходил по всем разделам и записывал каждый HTTP-запрос и ответ. Я запустил его у себя, он отработал за несколько минут и сохранил JSON со всем трафиком и скриншоты.
Что выяснилось:
- Почти все страницы отдают HTML, но есть один эндпоинт /timeline-data. Он возвращает в JSON все события сразу: оценки, пропуски, поведение. Это и стало основой бота.
- Вход защищён по схеме Laravel: CSRF-токен и сессионные cookies.
- Сайт работает и на кириллице, и на латинице.
- На каждый учебный год у ребёнка свой отдельный ID. У меня их было четыре — с 22/23 по 25/26. Этот факт ещё дважды выстрелит.
3. Первая версия на ноутбуке
Первую версию я запустил на MacBook в Docker. Стек: Python, python-telegram-bot, httpx для запросов, SQLite через aiosqlite, библиотека cryptography для шифрования паролей.
Вход сделал в два шага: сначала email, потом пароль. Сообщения с паролем бот сразу удаляет из чата и предупреждает об этом. Пароли в базе хранятся зашифрованными.
Самой сложной частью оказалось не пропустить изменения и не наспамить ложными. Я сделал две проверки:
- каждые 15 минут бот забирает первую страницу ленты и ищет новые и изменённые записи;
- раз в сутки забирает все страницы и ищет удалённые.
При первом запуске бот загрузил 114 событий: 53 оценки и 17 пропусков. Всё работало, но только пока ноутбук не спит.
4. Облако: дневник не пускает дата-центры
24 февраля я перенёс бота в Oracle Cloud. Взял бесплатную машину: 1 ядро, 1 ГБ памяти, Ubuntu 24.04, Франкфурт. Сразу настроил Docker, файрвол UFW, fail2ban, файл подкачки на 2 ГБ и резервное копирование по расписанию.
Бот запустился, но при входе выдал ошибку. Я проверил соединение с сервера: запрос к дневнику висел 15 секунд и падал по таймауту. Тот же запрос с ноутбука возвращал 200 OK.
Вывод: esDnevnik блокирует IP-адреса дата-центров. Скорее всего, не только Oracle, а любой облачный хостинг. Домашний IP при этом работает.
Варианты были такие: держать бота на ноутбуке или купить Raspberry Pi за 40–60 € и поставить дома. Я временно вернул бота на MacBook и стал думать, как оставить сервер в облаке, а в дневник ходить из дома.
5. Туннель через домашний роутер
Решение — WireGuard-туннель между облачным сервером и домашним роутером. Дома у меня GL.iNet Slate AX на OpenWrt со статическим IP. Бот живёт в облаке, но запросы к дневнику уходят через туннель домой и выходят в интернет с домашнего адреса. Дневник видит обычного домашнего пользователя.
Туннель поднялся сразу, handshake проходил, но трафик к дневнику не шёл. Ошибок было две:
- На роутере в правилах маршрутизации стояло правило с приоритетом 9920, которое отправляло весь трафик из WireGuard в «чёрную дыру». Пакеты пропадали ещё до файрвола. Я добавил правило с приоритетом 9900, которое срабатывает раньше и отправляет трафик в обычную таблицу маршрутов.
- На сервере в настройке WireGuard было разрешено только внутреннее адресное пространство туннеля. Адрес дневника туда не входил, и WireGuard отбрасывал пакеты за 1 мс. Я добавил адрес дневника в список.
Оба исправления я сделал постоянными: правило роутера прописал в автозапуск, туннель на сервере включил как системную службу. Ноутбук остановил. С этого дня бот работает в облаке круглосуточно.
6. Безопасность: туннель в домашнюю сеть — это риск
Сервер в облаке теперь имел дорожку в мою домашнюю сеть. Я нашёл 9 уязвимостей, разделил их на высокий, средний и низкий приоритет.
Что закрыл:
- Порт WireGuard на роутере был открыт всему интернету. Теперь он принимает пакеты только с IP моего сервера — правило в nftables.
- Ключи WireGuard лежали открыто. Поставил права 600, доступ только root.
- Файл с секретами бота закрыл правами 640 для группы docker. Сначала сделал строже, но тогда Docker не мог перезапустить контейнер. Пришлось найти баланс.
- Маршрутизацию сузил: роутер выпускает наружу не весь трафик туннеля, а только трафик с конкретного адреса сервера.
- Обновил прошивку, описал порядок смены токенов.
Отдельно сделал мониторинг. Скрипт каждые 30 минут проверяет четыре вещи: жив ли туннель, открывается ли дневник, работает ли контейнер и что отвечает сам бот на /health. Если что-то не так — присылает мне алерт в Telegram.
7. Бот работает, но молчит
Сразу после переезда я заметил: бот запущен, туннель жив, логин проходит, проверки идут — а уведомлений нет.
Я установил правило: сначала находим корневую причину, потом пишем код. Никаких мелких правок наугад.
Причина нашлась в разборе страницы. Бот искал учеников по ссылкам вида ?student= в переключателе учебных годов. Так он находил три прошлых года: 22/23, 23/24 и 24/25. А ID текущего года лежал не в ссылке, а в атрибуте Vue-компонента на странице. Бот его не видел.
Получилось, что бот исправно следил за тремя закрытыми годами, где новых оценок не будет никогда. Я переписал определение ученика: ID текущего года берётся из Vue-компонента, а планировщик проверяет только его.
В это же время у меня сменился пароль от дневника — пришлось заново пройти /login в боте.
8. Оплата и админка
17 марта я решил, что бот будет платным. Нужны были оплата и админ-панель.
Для оплаты выбрал Telegram Stars. Это встроенная валюта Telegram: пользователь платит прямо в боте, без внешней платёжной системы и без отдельного сайта. Цена — 250 Stars в месяц.
Главный вопрос был: что делать с тем, кто не платит. Я решил так: бот продолжает следить за его дневником и сохранять данные, но не присылает уведомления. Если отключить мониторинг совсем, то после оплаты на человека свалится лавина старых событий. А так он получает только новое.
Остальные решения:
- Неоплатившим раз в неделю, по понедельникам, приходит напоминание. Повторно одно и то же не шлётся.
- Продление считается от даты окончания текущей подписки, а не от сегодняшнего дня. Кто продлил заранее, ничего не теряет.
- Админка открывается только мне. В ней список пользователей и кнопка «дать бесплатный доступ». Бесплатный доступ хранится в той же таблице, что и платный, просто с другим типом.
- Я сам пользуюсь ботом без подписки.
До выкладки я нашёл и исправил пять ошибок: например, цена была захардкожена в двух местах, а для оплаты в Stars не хватало обязательного пустого параметра провайдера.
В этот же день я завёл приватный репозиторий на GitHub. Сервер пушит в него через отдельный deploy-ключ.
9. Три языка и перевод записей учителей
Сербский интерфейс удобен местным, но не родителям-экспатам. 18 марта я сделал выбор языка: сербский, русский, английский.
Появился онбординг из трёх шагов: выбор языка, короткое описание бота и кнопка входа.
Отдельный вопрос — записи учителей. Они всегда на сербском. Я сравнил варианты: только оригинал, автоперевод, перевод по кнопке, оригинал с переводом рядом. Выбрал последний: бот показывает оригинал и ниже перевод в скобках. В замечаниях учителя важны нюансы, и оригинал под рукой страхует от кривого перевода.
Перевод делает Google Cloud Translation. Чтобы не платить за одно и то же дважды, переводы кэшируются в базе. Если пользователь выбрал сербский, перевод не запрашивается вовсе. Ключ API ограничен IP моего сервера и только сервисом перевода. Бесплатного лимита — 500 тысяч символов в месяц — хватает с запасом.
В каждом языке тогда было 62 строки интерфейса.
10. Меню, команды и ещё один баг с учебными годами
Я заметил проблему на себе: выбрал при входе сербский, а потом устал переводить. Поменять язык было негде. Плюс моя админка осталась на сербском, хотя я выбрал русский.
Сделал кнопку «Меню» слева от поля ввода. Список команд в ней тоже переведён на три языка. Добавил три команды:
- /help — что бот умеет и как работает.
- /summary — сводка за период: сколько оценок, пропусков и замечаний. Период выбирается кнопками.
- /feedback — отзыв разработчику. Сообщение приходит мне в того же бота. Отдельного бота поддержки я делать не стал — лишняя сущность.
Админку полностью перевёл: все строки теперь зависят от языка, который я выбрал.
Тут снова вылезли учебные годы. /status и /summary показывали данные за все годы сразу. Две попытки починить в лоб не сработали: фильтр по дате первой загрузки не работал, потому что все годы загрузились в один момент; отсечка по дате учебного года тоже промахнулась. Правильным решением оказался явный флаг «текущий год» в базе. При каждой проверке бот сбрасывает его у всех и ставит только тем, кого вернул дневник.
Строк интерфейса стало 118 на каждый язык.
Ещё один урок этого этапа — Docker кэширует слои сборки, и обновлённый код иногда не попадал в контейнер. С тех пор собираю без кэша.
11. Лето: ложная тревога
На каникулах бот молчал — оценок нет. И вдруг прислал алерт: туннель WireGuard упал.
Я проверил вручную. Handshake туннеля был 43 секунды назад, дневник отвечал 200, контейнер работал, бот отвечал «healthy». Аптайм — около 127 дней без перезапуска, 12 393 проверки, 2 активных пользователя.
Туннель не падал. Старая проверка смотрела на один-единственный пинг. Если роутер не ответил на него вовремя, скрипт считал туннель мёртвым. Я заменил её на проверку свежести handshake WireGuard: если он был меньше 180 секунд назад, туннель жив. Это отражает реальную передачу данных, а не случайную потерю пинга.
12. 1 сентября: бот спамит нулями
В первый день нового учебного года бот начал раз за разом присылать сообщение «Данные загружены: оценок 0, пропусков 0, всего событий 0».
Цепочка была такая. На новый учебный год дневник создал сыну новый ID. По новому ID в базе пусто — бот решил, что это первый запуск, и прислал сводку. Дневник в первый день тоже пустой, сохранять нечего. На следующей проверке в базе снова пусто — снова «первый запуск», снова сводка. Цикл.
Ошибка была не в парсере, а в логике: «первая загрузка сделана» вычислялась по наличию данных, а не хранилась. Я добавил в базу явный флаг. Он ставится после первой успешной проверки, даже если событий ноль. Сводка уходит один раз и только если есть что показать. Миграция базы запускается сама при старте бота.
После выкладки в Telegram стало тихо. Первая оценка нового года придёт обычным уведомлением.
Что получилось
За восемь месяцев бот прошёл путь от скрипта на ноутбуке до сервиса с подпиской, тремя языками и мониторингом. Хостинг по-прежнему стоит ноль.
| Слой | Что использую | Зачем |
|---|---|---|
| Язык и бот | Python 3.12, python-telegram-bot 21.10 | Основа бота |
| Данные дневника | httpx, BeautifulSoup + lxml | Запросы к JSON-ленте и разбор страницы |
| Хранилище | SQLite через aiosqlite | Пользователи, события, подписки, кэш переводов |
| Безопасность данных | cryptography (Fernet) | Шифрование паролей от дневника |
| Расписание | APScheduler | Проверки каждые 15 минут и раз в сутки |
| Разведка | Playwright | Запись трафика дневника на старте |
| Запуск | Docker Compose | Одинаковое окружение на ноутбуке и сервере |
| Хостинг | Oracle Cloud Free Tier, Ubuntu 24.04 | Бесплатный сервер 24/7 |
| Сеть | WireGuard, GL.iNet на OpenWrt, nftables | Выход к дневнику через домашний IP |
| Защита сервера | UFW, fail2ban | Файрвол и защита от перебора |
| Мониторинг | Скрипт по cron + /health | Алерты в Telegram |
| Оплата | Telegram Stars | Подписка 250 Stars в месяц |
| Перевод | Google Cloud Translation API v2 | Перевод записей учителей |
| Код | Приватный репозиторий GitHub | История изменений |
Цифры:
- 3 языка интерфейса, 118 строк в каждом.
- 114 событий по сыну при первой загрузке: 53 оценки и 17 пропусков.
- около 127 дней работы без перезапуска и 12 393 проверки к концу лета.
- 2 активных пользователя.
- 0 € в месяц за хостинг; перевод укладывается в бесплатные 500 тысяч символов.