Шаблон aiogram 3, к которому я возвращаюсь на каждом новом боте
Собрав один и тот же Telegram-бот с нуля три раза, я остановился и превратил это в шаблон: роутеры по функциям, сценарии FSM, middleware и настройка Docker, которая просто работает.
К третьему Telegram-боту я заметил, что каждый раз делаю одно и то же: копирую прошлый проект, удаляю обработчики, перенастраиваю конфиг и полдня воюю с одной и той же обвязкой, прежде чем написать хоть одну новую функцию. Это сигнал, что пора собрать шаблон, что я и сделал.
Первое, что чинит шаблон, это файл, который отращивает любой бот: один огромный модуль обработчиков, где вместе живут все команды, callback и сообщения. В aiogram 3 есть полноценный Router, поэтому я разделяю обработчики по функциям, и каждая владеет своим куском.
from aiogram import Router
from aiogram.filters import CommandStart
router = Router()
@router.message(CommandStart())
async def start(message):
await message.answer("Welcome.")
Каждая функция получает свой router.py, а одно место подключает их все через dp.include_router(feature.router). Добавить функцию теперь означает «добавить папку», а не «отредактировать файл на 500 строк».
Второе исправление это многошаговые диалоги. Спросить у пользователя имя, потом возраст, потом подтвердить. Сделайте это на if и флагах, и сойдёте с ума. FSM в aiogram вместо этого делает шаги явными.
from aiogram.fsm.state import State, StatesGroup
class Register(StatesGroup):
name = State()
age = State()
@router.message(Register.name)
async def got_name(message, state):
await state.update_data(name=message.text)
await state.set_state(Register.age)
await message.answer("How old are you?")
Состояние и его данные живут в Redis, а не в процессе. Это становится важным ровно в тот момент, когда вы запускаете больше одного воркера или перезапускаетесь ради деплоя. Тот же урок, который рано или поздно усваивает любой бэкенд, просто в костюме бота.
Всё сквозное уходит в middleware, а не повторяется в обработчиках. В шаблоне их три: throttle, чтобы один пользователь не мог долбить бота, инжектор сессии базы данных, чтобы обработчик получал готовую сессию, и i18n, чтобы ответы совпадали с языком пользователя. Обработчики остаются тонкими и занимаются одним делом.
class ThrottleMiddleware(BaseMiddleware):
async def __call__(self, handler, event, data):
if await self.too_fast(event.from_user.id):
return
return await handler(event, data)
Конфигурация это pydantic-settings, читающий окружение, поэтому в коде нет токенов, а один и тот же образ работает локально и в продакшене. И всё это Dockerfile плюс compose-файл с ботом, Postgres и Redis: пишете docker compose up, и оно запускается на ноутбуке или на свежем VPS, без археологии по README.
Ни одна из этих идей по отдельности не гениальна. Ценность в том, что они решены один раз и лежат в шаблоне, поэтому следующий бот начинается с «написать функцию», а не с «заново придумать обвязку».