Решил вот написать не для продвижения ради, а для конструктивной обратной связи, чтобы продолжить пилить проект, т.к. сейчас выбираю, что же дальше и во что это может вырасти. Сразу предупрежу: делал с ИИ, так что, если кого-то это триггерит, можно скипнуть статью. Да, очередной бот, но тема мне близка и хотелось сделать что-то свое.
Что имеем as is - pет‑проект о том, как я с нуля собрал и выкатил в прод Telegram‑бота, который напоминает о фокусе дня, считает выполнения, дает ачивки, мягко мотивирует, работает по таймзонам и крутится на под systemd.
Задача: один фокус в день без лишних сервисов
Я хотел решить личную проблему с дисциплиной: не сорваться на рутине и не забрасывать мелкие, но важные задачи вроде зарядки, пет‑проекта или учебы. Без новой аппки, регистраций и таск‑менеджеров — всё внутри Telegram.
Из этого родилось простое требование к продукту:
один активный фокус на пользователя;
два касания в день: утром напомнить, вечером спросить, как прошёл день;
лёгкая игровая оболочка: стрики, ачивки, уровень цели;
минимум трения: старт по команде, дальше всё через бота.
Результат — бот @focuscompanion_bot, которым я сейчас сам пользуюсь. а также допиливаю по обратной связи.
Функционал с точки зрения пользователя
Сценарий работы:
При старте бот проводит онбординг: имя, время утреннего и вечернего уведомления, часовой пояс, домен (работа/личное/здоровье) и формулировка фокуса.
Каждое утро приходит сообщение с напоминанием о фокусе.
Вечером бот просит отметить результат по цели: ✅ сделано, 🌓 частично, ❌ не сделано.
По /week показывает срез по цели: для коротких целей — «недельный» вид с 7‑дневной полосой, для длинных — агрегированную статистику без привязки к 7 дням.
По /streak показывает текущую и лучшую серию.
/achievements — список полученных ачивок по всем целям.
/time, /focus, /settings, /feedback — управление расписанием, целью, минимальным режимом (подсказали идею как раз) и обратной связью. чтобы пользователь мог сразу рассказать, что поправить.
Проект сознательно держу максимально простым, но с нормальной структурой.
быстро считать текущий и лучший стрик по активному фокусу;
строить простую недельную и долгую статистику;
выдавать ачивки за достижение порогов ACHIEVEMENT_THRESHOLDS
Онбординг и FSM
Онбординг сделан через FSM aiogram: каждое состояние отвечает за один шаг и хранит промежуточные данные.
Цепочка шагов:
/start → проверка, есть ли пользователь в БД.
Если новый:
имя;
утреннее время (morning_time);
вечернее время (checkin_time);
часовой пояс (выбор из фиксированного списка таймзон РФ);
домен (работа/личное/здоровье);
формулировка фокуса.
В FSM‑данных собираются все поля, в конце:
создаётся запись в focuses (активный фокус);
обновляются поля пользователя в users;
пользователь выводится на основной сценарий с кнопками чек‑ина.
Таймзоны и расписание
Одна из самых нетривиальных для пет‑проекта частей — таймзоны и расписание.
Подход:
При онбординге пользователь выбирает таймзону вручную из списка преднастроенных (например, Europe/Moscow, Asia/Yekaterinburg и т.д.).
В БД в users.timezone сохраняется строка с именем таймзоны.
APScheduler крутится в UTC и раз в минуту выполняет две джобы:
send_morning_focus
send_daily_checkins.
Схема работы джобы:
Берём всех пользователей, у кого last_morning_sent (или last_checkin_reminder_sent) не равен сегодняшней дате.
Для каждого:
конвертируем now_utc в локальное время через pytz.timezone(user.timezone);
сравниваем локальное время HH:MM с сохранённым morning_time / checkin_time.
Если «сейчас» совпадает с временем пользователя — шлём сообщение, отмечаем last_*_sent = today.
from datetime import datetime
from pytz import timezone as pytz_timezone
async def send_daily_checkins():
now_utc = datetime.now(pytz_timezone("UTC"))
today_str_utc = now_utc.strftime("%Y-%m-%d")
# Берём всех, кому ещё не слали вечернее сообщение сегодня
users = await get_users_for_evening(today_str_utc)
if not users:
return
ids_to_mark: list[int] = []
for user in users:
tg_id = user["tg_id"]
user_id = user["id"]
tz_name = user["timezone"] or "Europe/Moscow"
user_tz = pytz_timezone(tz_name)
now_user = now_utc.astimezone(user_tz)
today_str = now_user.strftime("%Y-%m-%d")
checkin_time_local = (user["checkin_time"] or "").replace(":", "")
if not checkin_time_local:
continue
current_time_local = now_user.strftime("%H%M")
if current_time_local != checkin_time_local:
continue
status = await get_today_checkin_status(user_id, today_str)
if status:
# уже есть чек-ин — просто шлём краткий summary
text = get_summary_text(status, user.get("name"))
await bot.send_message(tg_id, text)
else:
# нет чек-ина — шлём кнопки done/partial/fail
text = "Как прошёл день по твоему фокусу?"
await bot.send_message(tg_id, text, reply_markup=checkin_kb)
ids_to_mark.append(user_id)
if ids_to_mark:
await mark_evening_sent(ids_to_mark, today_str_utc)
Такое решение:
не плодит отдельные cron‑задачи под каждого;
работает одинаково для разных регионов РФ;
остаётся простым для отладки.
Статистика и ачивки
Чек‑ин за день — это запись в checkins с statusdone/partial/fail.
Дальше поверх этого:
Стрик: считаю количество подряд идущих дней с done или partial, начиная от последней даты с чек‑ином назад. Частичный день не ломает серию.
Недельная статистика:
для коротких целей (≤7 дней) показываю:
7‑дневную ленту статусов (эмодзи);
прогресс‑бар из 10 блоков, который использует формулу done + partial * 0.5;
текстовое резюме недели.
для длинных целей (>7 дней) — только агрегаты по всем дням:
сколько всего дней по цели;
сколько из них done / partial / fail;
без привязки к «неделе» и «7 дням».
Система ачивок:
есть массив порогов ACHIEVEMENT_THRESHOLDS = [3, 7, 14, 30, ...];
при каждом обновлении стрика вычисляется уровень get_achievement_level(streak);
если достигнут новый уровень и он выше сохранённого для данного focus_id — добавляется запись в achievements.
Деплой: GitHub → Timeweb VDS → systemd
Разработка и деплой у меня организованы так.
Локально:
Работа в репозитории GitHub (приватный).
Цикл: правки кода → локальный запуск бота → проверки онбординга и основных команд → git commit → git push.
перезапуск через systemctl restart discipline-bot.
Для удобства есть скрипт deploy.sh, который делает pull, обновляет зависимости и перезапускает сервис, что прям облегчило коммиты в последнее время, коих стало много. До этого я использовал специализированный «бот‑хостинг», но столкнулся с кешированием, гонкой нескольких процессов и сложностью отладки, поэтому переехал на обычный VDS с голым терминалом — это оказалось проще и предсказуемее, и кажется более профессионально.
Что дальше
Сейчас бот живёт в проде, ежедневно шлёт уведомления. Из следующих шагов вижу:
миграцию с SQLite на PostgreSQL (для более сложной аналитики и параллельной нагрузки);
расширенную статистику по неделям и месяцам (retention, «процент идеальных недель», распределение по статусам);
доработку системы ачивок и уровней сложности;
экспорт данных и, возможно, лёгкую веб‑панель для просмотра прогресса с десктопа;
полноценный i18n и английскую локализацию, чтобы можно было смело нести бота на Reddit и англоязычные площадки.
Вопрос к читателям Хабра: что бы вы обязательно добавили в такого рода «бот‑трекер дисциплины» с точки зрения метрик и UX, или может по процессу? И как можно продвинуть.
Заранее всем большое спасибо! Буду рад любой обратной связи!