Телеграм-бот-напоминалка на Spring Boot. Пользователь пишет /add, бот спрашивает
текст и время, а дальше сам присылает напоминание в нужный момент — с кнопками
"готово" / "удалить" прямо в сообщении.
Делал как второй пет-проект после shop-бэкенда, чтобы закрыть то, что туда не влезло: работу с Telegram Bot API, FSM для многошагового диалога и persistent-джобы для отложенных уведомлений.
| Слой | Технология |
|---|---|
| Язык / рантайм | Java 17, Spring Boot 3.2 |
| Telegram API | TelegramBots 9.5.0 — модульная библиотека (telegrambots-longpolling + telegrambots-client + telegrambots-meta), long polling |
| БД | PostgreSQL + Spring Data JPA + Flyway |
| Состояние диалога | Redis (FSM с TTL) |
| Отложенные напоминания | Spring ThreadPoolTaskScheduler |
| Инфраструктура | Docker + Docker Compose (app + postgres + redis) |
Бот работает через long polling, не вебхук — не нужен домен и TLS-сертификат,
поднимается локально одной командой. Все апдейты от Telegram идут через
TelegramUpdateDispatcher: команды (/start, /add, /list...) роутятся по имени,
а обычный текст — по текущему состоянию диалога (FSM), которое лежит в Redis
(session:{chatId}).
Update ──▶ TelegramUpdateDispatcher
├─ команда (/add, /list...) ──▶ соответствующий CommandHandler
├─ callback (кнопка) ──▶ CallbackQueryHandler
└─ обычный текст ──▶ смотрим BotState в Redis
├─ AWAITING_TASK_TEXT ──▶ AddTaskHandler.onTaskText
└─ AWAITING_TASK_TIME ──▶ AddTaskHandler.onTaskTime
/add — это три шага: спросили текст → спросили время → сохранили задачу в Postgres
и поставили job в ThreadPoolTaskScheduler на нужный момент.
Отдельный нюанс: ScheduledFuture живут только в памяти процесса — при рестарте бота
все запланированные джобы теряются. Поэтому ReminderSchedulerService при старте
перечитывает из базы все PENDING-задачи с due_at в будущем и пересоздаёт джобы —
без этого шага активные напоминания после любого деплоя молча пропадали бы.
cp .env.example .env # вписать токен от @BotFather в BOT_TOKEN
docker compose up --buildПоднимутся: приложение (:8081), PostgreSQL (:5432), Redis (:6379).
Без реального BOT_TOKEN приложение всё равно поднимется и пройдёт все тесты —
удобно для CI. Просто long polling не запустится, в логах будет warning:
BOT_TOKEN не задан — бот не запущен.
mvn spring-boot:runНужен свой Postgres и Redis на портах из application.yml (или переопредели через
переменные окружения DB_HOST, DB_PORT, REDIS_HOST, REDIS_PORT).
| Переменная | Обязательна | По умолчанию | Что это |
|---|---|---|---|
BOT_TOKEN |
да (для реального запуска) | — | токен от @BotFather |
BOT_USERNAME |
нет | — | username бота без @, для справки |
DB_HOST / DB_PORT |
нет | localhost / 5432 |
Postgres |
DB_NAME / DB_USER / DB_PASSWORD |
нет | reminderbot / reminderbot / reminderbot |
Postgres |
REDIS_HOST / REDIS_PORT |
нет | localhost / 6379 |
Redis |
| Команда | Что делает |
|---|---|
/start |
регистрация чата в базе, приветствие со списком команд |
/add |
добавить напоминание — бот спросит текст, потом время |
/list |
активные напоминания, каждое с кнопками ✅ "готово" / 🗑 "удалить" |
/cancel |
прервать текущий диалог (например, если начал /add и передумал) |
/help |
список команд + формат времени |
Формат времени при добавлении:
- относительный —
10m(через 10 минут),2h,1d - абсолютный —
2026-07-05 18:00(зона Europe/Vilnius захардкожена, см. ограничения ниже)
config/ — Spring-конфигурация: бин TelegramClient, Redis-сериализация, scheduler
security/ — (нет — у бота нет HTTP API, аутентификация не нужна)
entity/ — AppUser, Task, TaskStatus
repository/ — Spring Data JPA поверх Postgres
fsm/ — BotState, UserSession, SessionService (Redis)
bot/ — диспетчер апдейтов, обработчики команд, клавиатуры, отправка сообщений
service/ — UserService, TaskService, ReminderSchedulerService
util/ — DateTimeParser
- Вебхук как альтернатива long polling — не добавлял, для пет-проекта overkill (нужен публичный HTTPS с валидным сертификатом).
- Таймзона пользователя — поле
app_users.timezoneв базе уже есть, ноDateTimeParserего пока не читает: у всех время интерпретируется как Europe/Vilnius. - Пагинация в
/listотсутствует — если у пользователя накопится полсотни напоминаний, бот честно зафлудит чат полусотней сообщений подряд. - Повторная отправка — если
TelegramApiExceptionвылетит при отправке напоминания (сеть, бот заблокирован пользователем и т.п.),ReminderSchedulerServiceпросто залогирует ошибку, retry нет. - Тесты — только контекстный тест на подъём приложения; юнит-тестов на
DateTimeParserи переходы FSM нет.
Если собираешь на свежей JDK (21+), а особенно на самых новых (23+), можешь упереться
в несовместимость версий транзитивных зависимостей, которые spring-boot-starter-parent
подтягивает по умолчанию, — они не успевают за новыми JDK:
- Lombok — старая managed-версия падает на новых
javacсExceptionInInitializerError/TypeTag :: UNKNOWN. Вpom.xmlверсия зафиксирована явно через<lombok.version>. - Mockito / Byte Buddy — managed-версии не умеют мокать классы на самых новых JDK
(
Java XX is not supported by the current version of Byte Buddy). Обе версии зафиксированы явно (<mockito.version>,<byte-buddy.version>). - TelegramBots 9.x — библиотека в 9.x прошла крупный рефакторинг вслед за
Telegram Bot API 7.0+ (
Messageпереехал в подпакетobjects.message,CallbackQuery.getMessage()теперь возвращаетMaybeInaccessibleMessage, а неMessage). Если после обновления версии библиотеки что-то не компилируется — первым делом смотри How To Update в документации авторов, там описаны все breaking changes по версиям.
Если апдейтишь версии сам и что-то ломается — это норм, библиотека молодая и активно переписывается, просто сверяйся с официальной документацией под конкретную версию, а не с примерами из интернета неизвестного возраста.