Как устроен мой таск-бот для Telegram
Написал бота-менеджера задач для семейного чата: Python, Firebase Cloud Functions и Firestore. Рассказываю, как он устроен внутри и на какие грабли я наступил с serverless.
У нас в семейном чате постоянно терялись дела. «Купи лампочку», «запишись к врачу», «почини кран» — всё это тонуло в переписке за пару дней. Списки в заметках не прижились: их надо открывать отдельно, а жизнь происходит в Telegram. Так появился @homework_taskbot — менеджер задач, который живёт прямо в чате.
Бот умеет ровно то, что нужно нам: создать задачу, взять её в работу, завершить, поставить оценку от одной до пяти звёзд. Считает, сколько времени задача провела в работе, причём честно суммирует по сессиям — взял в работу, отложил, вернулся. Дедлайн выбирается через встроенный календарь, к задаче можно писать комментарии. Ещё бот агрессивно чистит за собой: удаляет свои старые сообщения и команды пользователя, чтобы чат не превращался в свалку.
Стек: почему serverless
Мне не хотелось держать сервер ради бота, которым пользуются несколько человек. Поэтому вместо классической связки «VPS + polling» я выбрал вебхуки и Firebase:
- Python 3.11
- pyTelegramBotAPI в режиме webhook
- Firebase Cloud Functions 2-го поколения (регион
europe-west1) - Firestore как база данных
- GitHub Actions для деплоя
Telegram сам стучится в HTTPS-функцию при каждом сообщении. Нет апдейтов — нет запусков, нет запусков — нет расходов. Для домашнего бота это практически бесплатно.
Главная особенность serverless: у тебя нет памяти
Каждый вызов функции — потенциально новый процесс. Нельзя просто положить «пользователь сейчас вводит текст задачи» в глобальную переменную, как делают боты на long polling: следующее сообщение может прилететь в другой инстанс, который об этой переменной ничего не знает.
Поэтому конечный автомат состояний у меня живёт в Firestore, в коллекции
user_states. Бот спросил текст задачи — записал в базу состояние
awaiting_task_description. Пришло следующее сообщение — обработчик сначала
смотрит состояние в базе и только потом проверяет команды. Тот же трюк с
комментариями и календарём дедлайнов.
Вторая ловушка — инициализация. Если создавать клиент Firestore на уровне импорта модуля, деплой падает: приложение Firebase ещё не инициализировано. Пришлось делать ленивые синглтоны: и клиент базы, и объект бота создаются при первом обращении, а тёплый инстанс функции переиспользует их между вызовами.
Архитектура
Код разложен по слоям, хотя проекту это может показаться избыточным — всего около 1300 строк:
main.py — приём вебхука, разбор апдейта
update_processor.py — таблица маршрутов + диспетчер состояний
handlers.py — вся работа с Telegram API
task_manager.py — бизнес-логика, ничего не знает о Telegram
repositories.py — доступ к Firestore
models.py — датаклассы Task и Comment
views.py — тексты сообщений и клавиатуры
Смысл в том, что бизнес-логику можно тестировать без Telegram и без базы. Переходы между статусами задачи описаны явным словарём разрешённых переходов: из «новой» можно в работу или в архив, из архива — никуда. Попытка невалидного перехода просто отклоняется.
Отдельно горжусь нумерацией задач. Каждая задача в чате получает свой номер
(«Задача #42»), и счётчик инкрементируется в транзакции Firestore — два
одновременных сообщения не получат одинаковый номер. Комментарии дописываются
через ArrayUnion, тоже атомарно, без чтения-модификации-записи.
Из мелочей, которые заняли неприлично много времени: правильное склонение русских слов в отчётах о времени. «1 день 3 часа 2 минуты», а не «1 дней».
Деплой
Пуш в master запускает GitHub Actions, который деплоит функцию в Firebase.
Авторизация — через Workload Identity Federation, то есть без JSON-ключей
сервисного аккаунта в секретах репозитория: GitHub и Google Cloud доверяют друг
другу по OIDC. Токен бота лежит в GitHub Secrets и прокидывается в функцию как
переменная окружения. Откат — перезапуск любого предыдущего успешного воркфлоу.
Локальных эмуляторов у меня нет, тестовая среда — это прод. Для семейного бота
такой уровень риска меня устраивает, хотя юнит-тесты в репозитории есть:
бизнес-логика и роутинг покрыты через unittest с замоканным Firebase.
Попробовать
Бот открытый и бесплатный. Найдите @homework_taskbot
в Telegram (или просто перейдите по ссылке), нажмите /start — появится
клавиатура с кнопками. Можно пользоваться один на один с ботом, а можно добавить
в семейный чат: тогда боту нужны права администратора, чтобы удалять сообщения.
Исходный код лежит на GitHub: github.com/filimonovadm/homework_taskbot. Если соберётесь делать своего serverless-бота — там есть что подсмотреть, от структуры проекта до настройки деплоя.