Урок 79 из 84 · Telegram-бот на aiogram
FSM: анкеты и диалоги
Содержание урока
Представьте бота для записи на курс. Он должен спросить имя, потом возраст, потом телефон — и всё это по очереди. Но каждый ответ пользователя приходит отдельным сообщением, и хэндлер не помнит, о чём бот спрашивал секунду назад.
Эту задачу решает FSM (Finite State Machine, конечный автомат) — механизм состояний. Бот запоминает для каждого пользователя: «сейчас я жду от тебя имя» или «сейчас жду возраст».
Как это выглядит со стороны пользователя
Пользователь: /register
Бот: Как вас зовут?
Пользователь: Айбек
Бот: Сколько вам лет?
Пользователь: двадцать
Бот: Напишите возраст цифрами, например 20
Пользователь: 20
Бот: Отправьте номер телефона
Пользователь: +996 555 123 456
Бот: Готово! Айбек, 20 лет, +996 555 123 456
Шаг 1. Описываем состояния
Состояния — это «шаги» анкеты. Описываются классом:
from aiogram.fsm.state import State, StatesGroup
class Register(StatesGroup):
name = State() # ждём имя
age = State() # ждём возраст
phone = State() # ждём телефон
Шаг 2. Пишем хэндлеры
Каждому хэндлеру aiogram может передать объект state: FSMContext — через него мы меняем состояние и храним ответы.
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
router = Router()
@router.message(Command("register"))
async def start_register(message: Message, state: FSMContext):
await state.set_state(Register.name) # переходим к шагу «имя»
await message.answer("Как вас зовут?")
@router.message(Register.name, F.text)
async def got_name(message: Message, state: FSMContext):
await state.update_data(name=message.text) # запоминаем ответ
await state.set_state(Register.age) # следующий шаг
await message.answer("Сколько вам лет?")
@router.message(Register.age, F.text)
async def got_age(message: Message, state: FSMContext):
if not message.text.isdigit():
# Состояние не меняем — бот снова ждёт возраст
await message.answer("Напишите возраст цифрами, например 20")
return
await state.update_data(age=int(message.text))
await state.set_state(Register.phone)
await message.answer("Отправьте номер телефона")
@router.message(Register.phone, F.text)
async def got_phone(message: Message, state: FSMContext):
await state.update_data(phone=message.text)
data = await state.get_data() # всё, что собрали
await state.clear() # анкета закончена, сбрасываем состояние
await message.answer(f"Готово! {data['name']}, {data['age']} лет, {data['phone']}")
Главное здесь — фильтр по состоянию: @router.message(Register.age, F.text) сработает, только если пользователь сейчас на шаге «возраст». Поэтому одно и то же слово «20» в разных состояниях попадёт в разные хэндлеры.
Методы FSMContext
| Метод | Что делает |
|---|---|
await state.set_state(Register.age) |
Переключает пользователя на нужный шаг |
await state.update_data(age=20) |
Добавляет данные к уже собранным |
await state.get_data() |
Возвращает словарь со всеми собранными данными |
await state.get_state() |
Текущее состояние (или None) |
await state.clear() |
Сбрасывает и состояние, и данные |
Кнопка «Отмена»
Пользователь должен иметь возможность выйти из анкеты в любой момент. Хэндлер отмены ставим выше остальных хэндлеров анкеты:
from aiogram.filters import StateFilter
@router.message(StateFilter("*"), Command("cancel"))
async def cancel(message: Message, state: FSMContext):
if await state.get_state() is None:
await message.answer("Отменять нечего 🙂")
return
await state.clear()
await message.answer("Регистрация отменена")
StateFilter("*") означает «в любом состоянии».
Где хранятся состояния
По умолчанию aiogram хранит состояния в оперативной памяти (MemoryStorage). Это удобно для разработки, но после перезапуска бота все незаконченные анкеты пропадут.
В продакшене состояния хранят в Redis — они переживут перезапуск:
pip install redis
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://localhost:6379/0")
dp = Dispatcher(storage=storage)
Код хэндлеров при этом не меняется ни на строчку.
Практика
- Добавьте в анкету шаг «город» между возрастом и телефоном.
- На шаге телефона покажите reply-кнопку «📱 Отправить номер» с
request_contact=Trueи примитеF.contact. - В конце покажите inline-кнопки «✅ Всё верно» и «✏️ Заполнить заново».