Решил вот написать не для продвижения ради, а для конструктивной обратной связи, чтобы продолжить пилить проект, т.к. сейчас выбираю, что же дальше и во что это может вырасти.
Сразу предупрежу: делал с ИИ, так что, если кого-то это триггерит, можно скипнуть статью.
Да, очередной бот, но тема мне близка и хотелось сделать что-то свое.
Что имеем as is – pет‑проект о том, как я с нуля собрал и выкатил в прод Telegram‑бота, который напоминает о фокусе дня, считает выполнения, дает ачивки, мягко мотивирует, работает по таймзонам и крутится на VDS под systemd.
Задача: один фокус в день без лишних сервисов
Я хотел решить личную проблему с дисциплиной: не сорваться на рутине и не забрасывать мелкие, но важные задачи вроде зарядки, пет‑проекта или учебы. Без новой аппки, регистраций и таск‑менеджеров — всё внутри Telegram.
Из этого родилось простое требование к продукту:
-
один активный фокус на пользователя;
-
два касания в день: утром напомнить, вечером спросить, как прошёл день;
-
лёгкая игровая оболочка: стрики, ачивки, уровень цели;
-
минимум трения: старт по команде, дальше всё через бота.
Результат — бот @focuscompanion_bot, которым я сейчас сам пользуюсь. а также допиливаю по обратной связи.
Функционал с точки зрения пользователя
Сценарий работы:
-
При старте бот проводит онбординг: имя, время утреннего и вечернего уведомления, часовой пояс, домен (работа/личное/здоровье) и формулировка фокуса.
-
Каждое утро приходит сообщение с напоминанием о фокусе.
-
Вечером бот просит отметить результат по цели:
✅ сделано,🌓 частично,❌ не сделано. -
По
/weekпоказывает срез по цели: для коротких целей — «недельный» вид с 7‑дневной полосой, для длинных — агрегированную статистику без привязки к 7 дням. -
По
/streakпоказывает текущую и лучшую серию. -
/achievements— список полученных ачивок по всем целям. -
/time,/focus,/settings,/feedback— управление расписанием, целью, минимальным режимом (подсказали идею как раз) и обратной связью. чтобы пользователь мог сразу рассказать, что поправить.

Проект сознательно держу максимально простым, но с нормальной структурой.
Стек:
-
Python 3.12
-
aiogram 3 (асинхронный Telegram‑фреймворк)
-
SQLite в качестве хранилища (пока)
-
APScheduler для периодических задач
-
systemd‑service для запуска на VDS Timeweb
Файлы проекта:
-
bot.py— основная логика, хендлеры команд, FSM онбординга и настроек, планировщик APScheduler. -
db.py+models.sql— слой работы с SQLite и схема БД. -
config.py— чтение.env, токен бота засунул в итоге в переменные потом, словари ачивок и порогов. -
discipline.db— сама база. -
requirements.txt— зависимости. -
images/— в том числе приветственный экран / стартовая картинка.
Бот полностью на async/await: aiogram 3, FSM из коробки, асинхронный APScheduler.
Модель данных
Минимальный набор сущностей:
-
users:-
id,tg_id -
name -
morning_time,checkin_time(строкиHH:MM) -
start_date -
timezone(строка типаEurope/Moscow) -
служебные поля:
last_morning_sent,last_checkin_reminder_sent,minimal_mode.
-
-
focuses:-
id -
user_id -
title(формулировка цели) -
domain(работа/личное/здоровье) -
is_active,started_at,ended_at -
best_streak(лучший стрик по этой цели).
-
-
checkins:-
id -
user_id,focus_id -
date(YYYY‑MM‑DD) -
status(done,partial,fail).
-
-
achievements:-
id -
user_id,focus_id -
level(номер уровня ачивки) -
days(длина серии на момент получения) -
created_at.
-
Такой набор позволяет:
-
быстро считать текущий и лучший стрик по активному фокусу;
-
строить простую недельную и долгую статистику;
-
выдавать ачивки за достижение порогов
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 с status done/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.
На сервере (VDS Timeweb):
-
В каталоге проекта:
-
git pull origin main -
pip install -r requirements.txt(при необходимости) -
миграции схемы через
models.sql(минимальные DDL).
-
-
Бот крутится как
discipline-bot.serviceпод systemd:-
ExecStart=/root/discipline_bot/venv/bin/python /root/discipline_bot/bot.py -
перезапуск через
systemctl restart discipline-bot.
-
-
Для удобства есть скрипт
deploy.sh, который делает pull, обновляет зависимости и перезапускает сервис, что прям облегчило коммиты в последнее время, коих стало много.
До этого я использовал специализированный «бот‑хостинг», но столкнулся с кешированием, гонкой нескольких процессов и сложностью отладки, поэтому переехал на обычный VDS с голым терминалом — это оказалось проще и предсказуемее, и кажется более профессионально.
Что дальше
Сейчас бот живёт в проде, ежедневно шлёт уведомления. Из следующих шагов вижу:
-
миграцию с SQLite на PostgreSQL (для более сложной аналитики и параллельной нагрузки);
-
расширенную статистику по неделям и месяцам (retention, «процент идеальных недель», распределение по статусам);
-
доработку системы ачивок и уровней сложности;
-
экспорт данных и, возможно, лёгкую веб‑панель для просмотра прогресса с десктопа;
-
полноценный i18n и английскую локализацию, чтобы можно было смело нести бота на Reddit и англоязычные площадки.
Ссылка на бота: https://t.me/focuscompanion_bot.
Вопрос к читателям Хабра: что бы вы обязательно добавили в такого рода «бот‑трекер дисциплины» с точки зрения метрик и UX, или может по процессу? И как можно продвинуть.
Заранее всем большое спасибо! Буду рад любой обратной связи!
Автор: teaglaim


