Как Telegram-бот для дисциплины дорос до 5k строк, рефералок и тотемных животных (и что я об этом думаю)

Статья о боте, который превратился из игрушки и хобби в проект с овер 100 юзерами и 5к строчками кода.
Статья о боте, который превратился из игрушки и хобби в проект с овер 100 юзерами и 5к строчками кода.

1Дисклеймер: Я не профессиональный разработчик. В коде разбираюсь на уровне продакта - то есть знаю, чего хочу, и примерно представляю, как это должно работать. Код пишу с ИИ, сам правлю, сам ломаю прод (ну, почти) и сам чиню. Эта статья - скорее рефлексия о том, как пет-проект превращается в продукт, какие архитектурные решения (спонтанные или осознанные) этому помогают, а какие - мешают. И конечно, мне очень нужна обратная связь от тех, кто в этом понимает больше меня.

2Дисклеймер: я все понимаю, что происходит с ТГ и отдаю себе в этом отчет.

UPD: Предыдущие части можно почитать тут: ссылка на первую статью и ссылка на вторую. Если кратко, то начиналось всё с простой идеи: бот каждый день спрашивает «Ты сделал то, что планировал?» и ведёт статистику.

Что случилось за два месяца?

Прошло два месяца. Код раздулся до 5к+ строк. Пользователей, которые хоть раз зашли - под 120. Появилась куча нового функционала: челленджи (для хардкорных), квесты (для дофамина), реферальная программа (для виральности), тотемные животные (для самоидентификации) и стили общения (чтобы не надоедал). Главное, чему я научился - почти не ломать продакшен. Почти.

В этой статье не будет сухого перечисления фич. Я хочу показать почему они появились именно в таком виде, как я их технически реализовал (со всеми граблями) и какие выводы из этого сделал.

О чём вообще этот бот (ещё раз)

Для меня дисциплина - это не про героизм в 5 утра. Это про умение делать маленькие шаги к тому, что важно, даже когда не хочется. И ключевое слово здесь - регулярность. Мой бот не мотивирует, не кричит. Он просто напоминает: «Эй, сегодня у тебя был фокус. Как успехи?» и честно показывает статистику.

Почему это работает? Потому что, когда ты видишь серию в 14 дней, рука не поднимается нажать «не сделано». А если сорвался - бот не добивает, а говорит: «бывает, завтра новый день». Это та самая «мягкая сила», которая работает лучше любых угроз самому себе.

И важный принцип: бот не должен раздражать. Хочешь тишины - два касания в день. Хочешь развлечения - включай фичи. Всё просто.

1. Как я (наконец-то) подружился с таймзонами и планировщиком

Это была, пожалуй, самая выматывающая задача. Отправлять уведомления в локальное время каждого пользователя оказалось нетривиально. Наивно полагал, что можно как-то автоматически определить таймзону по IP или данным профиля - нет, нельзя. Пришлось спрашивать вручную при онбординге.

Как работает сейчас: Каждую минуту APScheduler дёргает две функции: send_morning_focus и send_daily_checkins. Внутри них я получаю всех пользователей с заданным временем, конвертирую UTC в их локальное время и сравниваю.

Грабли №1: Сравнивать время как строки - плохая идея. «08:30» > «23:00»? Нет, не сработает. Поэтому я перевожу всё в минуты с помощью простой функции:

def time_to_minutes(t: str) -> int: h, m = map(int, t.split(':')) return h * 60 + m

Теперь сравнение current_minutes < morning_minutes работает корректно, даже если локальное время перевалило за полночь.

Грабли №2 (пользовательские): Что делать, если юзер отправил бота в бан? Раньше логи забивались ошибками 403 при каждой попытке отправить сообщение. Теперь я ловлю исключение и просто отключаю уведомления этому пользователю:

except Exception as e: error_str = str(e) if "Forbidden: bot was blocked by the user" in error_str or "403" in error_str: # Отключаем уведомления async with aiosqlite.connect(DB_PATH) as db: await db.execute("UPDATE users SET morning_time = NULL WHERE tg_id = ?", (tg_id,)) await db.commit() logging.warning(f"Пользователь {tg_id} заблокировал бота. Уведомления отключены.") continue # ... остальные ошибки

Это не только чистит логи, но и снижает бесполезную нагрузку на бота и API Telegram.

Вывод: Простые решения (сравнение в минутах, отключение банов) часто работают лучше сложных.

2. Реферальная система и feature flags: как я сделал виральность и тут же её сломал

Рефералка - центральная механика для открытия дополнительных фич. Выбор осознанный: хочешь больше возможностей - приведи друзей, которые тоже будут пользоваться ботом. Это и виральность, и фильтр «своих».

Как устроено:

  • У каждого пользователя есть уникальный referral_code.
  • В БД есть таблица referrals, где хранятся связи «кто кого пригласил» и сколько чек-инов сделал реферал (исключая дни отдыха).
  • Раз в 30 минут фоновая задача check_referrals_job проверяет прогресс. Как только у реферала набирается 3 чек-ина, счётчик referrals_completed у пригласившего увеличивается.
  • Когда referrals_completed достигает 3, пригласившему выдаются фичи: челленджи, стили общения, шаблоны целей. Всё это хранится в JSON-поле feature_flags в таблице users.

Грабли (классические): Я использовал одно и то же поле message_style и для флага доступа, и для хранения текущего стиля. При достижении 3 рефералов я писал: flags["message_style"] = True. А когда пользователь менял стиль командой, я писал: flags["message_style"] = "neutral". Второй вызов затирал True, и доступ к разделу стилей пропадал. Пользователи писали: «Я открыл стили, но они не работают!».

Решение: Разделить на два флага. Теперь есть styles_unlocked (булевый) и current_style (строка). При выдаче награды:

flags["challenges"] = True flags["styles_unlocked"] = True flags["current_style"] = "neutral" flags["templates"] = True

Вывод: Не экономьте на флагах. Одна сущность - одна ответственность. JSON поле оказалось очень гибким (не требует миграций БД при добавлении новой фичи), но у него есть минус: сложно сделать SQL-запрос типа «найти всех с открытыми челленджами». Для 120 юзеров - ок, для 12 000 - придётся выносить в отдельную таблицу. Будем посмотреть.

3. Челленджи и награды: когда скучно просто отмечаться

Когда базовая механика приелась, я добавил челленджи. Это добровольное усложнение с наградами для тех, кому хочется разнообразия.

Как работает: Пользователь выбирает уровень (Лёгкий, Продвинутый, Экстра). Для каждого уровня заданы длительность, допустимое количество частичных выполнений и награда. Данные хранятся в отдельной таблице challenges.

Логика обработки чек-ина вынесена в отдельную функцию process_challenge_checkin. Вот её упрощённая схема для статуса done:

if status == "done": new_day = challenge['current_day'] + 1 if new_day >= level_info['days']: # Вызов выполнен! await complete_challenge(...) await grant_challenge_rewards(...) return True, "🎉 Поздравляю! Вызов выполнен!" else: await update_challenge_progress(...) return False, None

Награды:

  • Ачивка в профиль.
  • Бонусные дни отдыха (копятся и увеличивают лимит в месяц).
  • За средний уровень - открывается эксклюзивный стиль общения «Легенда».
  • За хард-уровень - пользователь оставляет себе сообщение в будущее, которое бот пришлёт после победы. Трогательная штука.

Вывод: Геймификация работает, если она не навязана. Челленджи - опция. Кто хочет - играет, кто не хочет - живёт в спокойном режиме.

4. Тотемные животные: дата драйвен классификация без нейросетей

Самая неожиданная фича, которая залетела многим. В разделе /ranking бот показывает твоё тотемное животное на основе реальной статистики.

Алгоритм (calculate_totem_animal) простой, как три рубля одной монетой, но работает:

def calculate_totem_animal(stats: dict) -> str: best_streak = stats.get("best_streak", 0) total_green = stats.get("total_green", 0) if best_streak >= 30: return "орёл" if total_green > 100 else "лев" if best_streak >= 14: return "лев" if total_green > 50 else "волк" if best_streak >= 7: return "волк" if total_green > 20 else "бизон" if total_green > 10: return "бизон" if total_green > 5: return "леопард" return "черепаха"

Метрики всего две:

  • best_streak - лучшая серия за всё время (показывает максимальную собранность).
  • total_green - общее количество дней со статусом «сделано» (показывает общий объём работы).

Комбинация этих двух чисел даёт вполне осмысленные архетипы. Черепаха - только начинает. Леопард - срывается, но быстро возвращается. Волк - стабильный середняк. Лев - лидер с длинными сериями. Орёл - ветеран с огромным опытом.

Вывод: Для крутой фичи не всегда нужен сложный AI. Иногда достаточно простой, понятной и, главное, честной логики. Пользователи любят смотреть на свой прогресс через такие метафоры.

Что дальше?

Проект живёт, пользователи есть, обратная связь - бесценна. В планах:

  1. Улучшить аналитику для себя (понять ретеншн, конверсию).
  2. Подумать над оптимизацией БД (те 30-минутные запросы по всем рефералам уже сейчас заставляют задуматься об индексах и кешировании).
  3. Сделать рефакторинг bot.py, который превратился в монстра на кучу+ строк. Понятно, что пора разбивать на модули, но рука не поднимается.

Если зашло, юзайте бота https://t.me/focuscompanion_bot Или следите за "кухней" в канале https://t.me/discipline_bot_consigliere

Всем добра!

Как всегда, буду рад конструктивной критике и советам от сообщества. Спасибо, что дочитали!