Appearance
Aiogram 3: Finite State Machine - автомата состояний
Тема реализации конечных автоматов состояний FSM в телеграм-ботах на Aiogram 3.x.
Поднятие Redis сервера: Полное руководство (Redis будем использовать в качестве хранилища для FSM)
Также советую развернуть Redis сервер и использовать его в качестве хранилища данных для FSM. Но, если не хотите заморачиваться Redis, я покажу, как использовать его аналог MemoryStorage и расскажу почему этого лучше не делать.
Что мы будем делать сегодня?
Сегодня разберем FSM на конкретном примере: настроим анкету пользователя, захватим его логин, имя, возраст, фото и информацию о себе, а затем отобразим эти данные.
После изучения материала вы полностью овладеете навыком взаимодействия с FSM в Aiogram 3.x, и останется только научиться записывать эти данные в базу данных PostgreSQL.
Установка необходимых модулей
Для начала установим все необходимые модули:
shell
pip install aiogram python-decouple redisredis — для взаимодействия с базой данных Redis. python-decouple — для работы с .env файлами. aiogram — библиотека для создания ботов.
Доступ к Redis
- Хост
- Порт
- Номер базы данных (по умолчанию от 0 до 15)
- (Опционально) Логин и пароль, если они заданы
Пример строки подключения к Redis без логина и пароля:
shell
redis://HOST:PORT/NUM_DBПример строки подключения к Redis с логином и паролем:
python
redis://LOGIN:PASSWORD@HOST:PORT/NUM_DBНастройка create_bot.py
В файле create_bot.py создадим объект storage для хранения данных FSM. Импортируем переменные из .env файла:
python
from decouple import config
redis_url = config('REDIS_URL')Настройка Storage
Импортируем модуль RedisStorage:
python
from aiogram.fsm.storage.redis import RedisStorageЭто импортирует класс RedisStorage из библиотеки aiogram, который используется для хранения данных конечного автомата состояний FSM в Redis.
Создаем объект RedisStorage:
python
storage = RedisStorage.from_url(config('REDIS_URL'))Здесь мы создаем объект RedisStorage, используя URL подключения к Redis, который берем из переменной окружения REDIS_URL, загруженной с помощью config из библиотеки python-decouple.
Инициализация Dispatcher с RedisStorage:
python
dp = Dispatcher(storage=storage)Мы создаем объект Dispatcher, передавая ему наш storage для хранения состояния конечного автомата в Redis. Это позволяет боту сохранять и восстанавливать состояния пользователей, используя Redis как хранилище.
Если вы не хотите использовать Redis в качестве хранилища, можно использовать MemoryStorage:
python
from aiogram.fsm.storage.memory import MemoryStorage
dp = Dispatcher(storage=MemoryStorage())MemoryStorage использует оперативную память, что приводит к полной потере данных при любом сбое на сервере или в боте.
Redis так-же использует оперативную память для хранения данных, но в отличие от MemoryStorage, поддерживает периодическую запись данных на диск и может работать в кластерной среде, обеспечивая масштабируемость и надежность системы.
Таким образом, использование RedisStorage в FSM для Telegram-ботов обеспечивает высокую производительность и надежность, что делает его предпочтительным выбором для любых телеграмм ботов, но, в учебных целях, можете использовать MemoryStorage.
Сейчас приведу 2 примера, которые явно продемонстрируют отличия.
- Запускаем некий сценарий анкетирования (всего 5 вопросов)
- После третьего вопроса перезагружаем бота
Если использовалась MemoryStorage, то все данные будут потеряны и сценарий нужно будет начинать сначала. При использовании Redis - сценарий для каждого пользователя продолжится с того места, где тот остановился.
Что такое FSM?
FSM, или конечный автомат состояний Finite State Machine, — это простой способ управлять сложными взаимодействиями в вашем Telegram боте. Он помогает боту "запомнить", на каком шаге процесса находится пользователь и что делать дальше.
К примеру создадим анкету, которая будет вести пользователя через следующие шаги:
- Сначала спрашивает пол.
- Затем возраст.
- Потом имя и фамилию.
- Далее логин.
- Попросит отправить фото.
- И наконец, попросит добавить описание о себе.
FSM помогает боту отслеживать, на каком из этих шагов находится пользователь, и что спросить дальше. Если пользователь отправил имя, бот "запоминает" это и знает, что следующий шаг — запросить логин.
Настройка первого скрипта FSM
Импортируем необходимые модули:
python
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroupОпределим состояния:
python
class Form(StatesGroup):
name = State()
age = State()Под каждое состояние напишем отдельные хендлеры, которые будут реагировать на ввод текста (имя и возраст):
python
import asyncio
from create_bot import bot
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from aiogram.utils.chat_action import ChatActionSender
import re
def extract_number(text):
match = re.search(r'\b(\d+)\b', text)
return int(match.group(1)) if match else None
class Form(StatesGroup):
name = State()
age = State()
questionnaire_router = Router()
@questionnaire_router.message(Command('start_questionnaire'))
async def start_questionnaire_process(message: Message, state: FSMContext):
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Привет. Напиши как тебя зовут: ')
await state.set_state(Form.name)
@questionnaire_router.message(F.text, Form.name)
async def capture_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Супер! А теперь напиши сколько тебе полных лет: ')
await state.set_state(Form.age)
@questionnaire_router.message(F.text, Form.age)
async def capture_age(message: Message, state: FSMContext):
check_age = extract_number(message.text)
if not check_age or not (1 <= check_age <= 100):
await message.reply('Пожалуйста, введите корректный возраст (число от 1 до 100).')
return
await state.update_data(age=check_age)
data = await state.get_data()
msg_text = (f'Вас зовут <b>{data.get("name")}</b> и вам <b>{data.get("age")}</b> лет. '
f'Спасибо за то что ответили на мои вопросы.')
await message.answer(msg_text)
await state.clear()Импортировали FSMContext из aiogram.dispatcher, а также State и StatesGroup из aiogram.dispatcher.filters.state для работы с конечным автоматом состояний FSM в нашем боте.
FSMContext:- Это специальный объект, который помогает нам управлять состояниями пользователя. Он хранит данные о текущем состоянии пользователя и позволяет изменять их, перемещая пользователя между различными состояниями.
- Пример использования: С помощью
FSMContextможем сохранить имя пользователя и затем перейти к следующему шагу, где спросим возраст.
StateиStatesGroup:Stateпредставляет собой конкретное состояние, в котором может находиться пользователь.StatesGroupпозволяет объединять несколько состояний в логическую группу.- Пример использования: создаем класс
Form, который наследуется отStatesGroup, и внутри него определяем состоянияnameиage. Это помогает нам структурировать и управлять последовательностью шагов анкеты.
Функция extract_number извлекает число из текста. Полезно на случай если пользователь вместо 20 будет писать мне 20 лет. Данная функция достанет 20 и сразу трансформирует это запись в int.
Теперь можете видеть, что у нас появился новый аргумент в функции — state. Он позволяет управлять состояниями пользователя, перемещать пользователя по состояниям и прочее.
Также под анкету создал новый роутер. Он нам пригодится далее, когда мы будем делать «боевую» анкету.
Для красоты добавил имитацию набора текста. Самое важное — это конструкция такого типа: await state.set_state(Form.name).
Эта запись указывает, что пользователь, когда дойдет до этого момента функции, окажется в состоянии Form.name, а значит, что его отправка данных (в нашем случае это ввод имени) окажется уже в этом состоянии.
Идем далее:
python
@questionnaire_router.message(F.text, Form.name)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(name=message.text)
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Супер! А теперь напиши сколько тебе полных лет:')
await state.set_state(Form.age)Здесь применили новый метод, а именно — сохранение данных от пользователя в переменную name при помощи state.update_data. После, просто перенесли пользователя в новое состояние Form.age.
python
@questionnaire_router.message(F.text, Form.age)
async def start_questionnaire_process(message: Message, state: FSMContext):
check_age = extract_number(message.text)
if not check_age or not (1 <= int(message.text) <= 100):
await message.reply("Пожалуйста, введите корректный возраст (число от 1 до 100).")
await state.set_state(Form.age)
else:
await state.update_data(age=check_age)
data = await state.get_data()
msg_text = (f'Вас зовут <b>{data.get("name")}</b> и вам <b>{data.get("age")}</b> лет. '
f'Спасибо за то, что ответили на мои вопросы.')
await message.answer(msg_text)
await state.clear()Здесь сначала мы проверили, есть ли в последнем сообщении пользователя возраст и находится ли он в диапазоне от 1 до 100. Если это не так, мы отправляем пользователя в состояние ввода возраста.
В случае, когда нужно просто оставить пользователя в том же состоянии при ошибке, можно использовать:
python
if not check_age or not (1 <= int(message.text) <= 100):
await message.reply("Пожалуйста, введите корректный возраст (число от 1 до 100).")
returnТем самым укажем, что нужно остаться там же, но я в таких случаях обычно явно прописываю, в какое состояние нужно отправиться (когда через полгода, например, к своему проекту возвращаешься — это становится очень полезным).
Если возраст был введен корректно, записываем возраст в хранилище (этого также можно было не делать, так как мы все равно в этом хендлере завершаем сценарий, но я считаю, что явное лучше неявного).
Для того чтобы достать данные из хранилища, использовали запись:
python
data = await state.get_data()В данном случае data — это обычный питоновский словарь, с которым можно делать все, что можно делать со словарями. Например, доставать значения по ключу.
Обратите особое внимание на:
python
await state.clear()Это нужно делать обязательно, когда завершаете сценарий. Если вы не завершите сценарий, бот не поймет, что возраст получен, и будет ждать его до бесконечности.
Сейчас рассмотрим самый частый случай у новичков.
Пользователь заполняет анкету, а после передумывает. Нажимает через командное меню /start, а у него ничего не происходит. Дело в том, что сценарий, который мы запустили, не завершился.
Бывает, что происходит. В таком случае вместо имени бот записывает имя /start, после отправляет новый вопрос «Введи возраст». Пользователь снова жмет /start, и это все идет до момента, пока пользователь не удаляет бота и не считает, что его делали некомпетентные люди.
Чтобы эту проблему избежать, стоит в своей архитектуре закладывать возможность выхода из сценария анкетирования. Всегда закладывайте в команде /start и в прочих командах (в их хендлерах) сброс сценария. Для этого необходимо следующее:
python
async def cmd_start(message: Message, state: FSMContext):
await state.clear()В таком случае вы автоматически ставите закрытие сценария анкетирования, и пользователь, нажав на старт, просто получит сброс данных.
Также советую добавлять возможность выхода по клику на кнопку клавиатуры. Это может быть текстовая кнопка с надписью «Отмена» или инлайн-кнопка с call_data = cancel, а далее просто обработчик, который будет закрывать (очищать) хранилище, тем самым выбивая пользователя из сценария.
На это акцентируйте особое внимание, так как проблем, связанных с «не выходом» из сценария, случается предостаточно.
Вот что получилось.

Надеюсь, что к настоящему моменту вы уловили общие принципы взаимодействия с FSM, а значит что мы можем приступать к заполнению «боевой» анкеты. Бот пойдет по такому сценарию:
- Сначала спрашивает пол (текстовая клавиатура с вариантами пола)
- Затем возраст (удалим клавиатуру)
- Потом имя и фамилию
- Далее логин (инлайн клавиатура с возможностью выбрать свой логин с профиля телеграмм если он есть)
- Попросит отправить фото (тут смысл в том чтоб захватить
file_idфото) - И наконец, попросит добавить описание о себе.
Сейчас просто перепишем код, добавив новые состояния, а в результате выведем данные, введенные пользователем, с вопросом «Все верно?» и вариантами «Все верно» и «Заполнить сначала» (этот вариант будет сбрасывать анкету и запускать её с момента ввода имени).
Текстовая клавиатура выбора пола:
python
def gender_kb():
kb_list = [
[KeyboardButton(text="👨🦱Мужчина")], [KeyboardButton(text="👩🦱Женщина")]
]
keyboard = ReplyKeyboardMarkup(keyboard=kb_list,
resize_keyboard=True,
one_time_keyboard=True,
input_field_placeholder="Выбери пол:")
return keyboardТут я использовал текстовую клавиатуру просто для примера. Обычно стараюсь использовать инлайн-клавиатуры.
Инлайн-клавиатура проверки заполнения данных:
python
def check_data():
kb_list = [
[InlineKeyboardButton(text="✅Все верно", callback_data='correct')],
[InlineKeyboardButton(text="❌Заполнить сначала", callback_data='incorrect')]
]
keyboard = InlineKeyboardMarkup(inline_keyboard=kb_list)
return keyboardИнлайн-клавиатура, которая позволит при клике использовать логин, указанный пользователем в ТГ:
python
def get_login_tg():
kb_list = [
[InlineKeyboardButton(text="Использовать мой логин с ТГ", callback_data='in_login')]
]
keyboard = InlineKeyboardMarkup(inline_keyboard=kb_list)
return keyboardТут ещё добавим проверку, чтобы, если логина в Телеграме не было, его необходимо было указать.
Вот полный код анкеты:
python
import asyncio
from create_bot import bot
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message, ReplyKeyboardRemove, CallbackQuery
from aiogram.utils.chat_action import ChatActionSender
from keyboards.all_kb import gender_kb, get_login_tg, check_data
from utils.utils import extract_number
class Form(StatesGroup):
gender = State()
age = State()
full_name = State()
user_login = State()
photo = State()
about = State()
check_state = State()
questionnaire_router = Router()
@questionnaire_router.message(Command('start_questionnaire'))
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.clear()
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Привет. Для начала выбери свой пол: ', reply_markup=gender_kb())
await state.set_state(Form.gender)
@questionnaire_router.message((F.text.lower().contains('мужчина')) | (F.text.lower().contains('женщина')), Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(gender=message.text, user_id=message.from_user.id)
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Супер! А теперь напиши сколько тебе полных лет: ', reply_markup=ReplyKeyboardRemove())
await state.set_state(Form.age)
@questionnaire_router.message(F.text, Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(name=message.text)
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Пожалуйста, выбери вариант из тех что в клавиатуре: ', reply_markup=gender_kb())
await state.set_state(Form.gender)
@questionnaire_router.message(F.text, Form.age)
async def start_questionnaire_process(message: Message, state: FSMContext):
check_age = extract_number(message.text)
if not check_age or not (1 <= int(message.text) <= 100):
await message.reply("Пожалуйста, введите корректный возраст (число от 1 до 100).")
return
await state.update_data(age=check_age)
await message.answer('Теперь укажите свое полное имя:')
await state.set_state(Form.full_name)
@questionnaire_router.message(F.text, Form.full_name)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(full_name=message.text)
text = 'Теперь укажите ваш логин, который будет использоваться в боте'
if message.from_user.username:
text += ' или нажмите на кнопку ниже и в этом случае вашим логином будет логин из вашего телеграмм: '
await message.answer(text, reply_markup=get_login_tg())
else:
text += ' : '
await message.answer(text)
await state.set_state(Form.user_login)
# вариант когда мы берем логин из профиля телеграмм
@questionnaire_router.callback_query(F.data, Form.user_login)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
await call.answer('Беру логин с телеграмм профиля')
await call.message.edit_reply_markup(reply_markup=None)
await state.update_data(user_login=call.from_user.username)
await call.message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
await state.set_state(Form.photo)
# вариант когда мы берем логин из введенного пользователем
@questionnaire_router.message(F.text, Form.user_login)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(user_login=message.from_user.username)
await message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
await state.set_state(Form.photo)
@questionnaire_router.message(F.photo, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
photo_id = message.photo[-1].file_id
await state.update_data(photo=photo_id)
await message.answer('А теперь расскажите пару слов о себе: ')
await state.set_state(Form.about)
@questionnaire_router.message(F.document.mime_type.startswith('image/'), Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
photo_id = message.document.file_id
await state.update_data(photo=photo_id)
await message.answer('А теперь расскажите пару слов о себе: ')
await state.set_state(Form.about)
@questionnaire_router.message(F.document, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
await message.answer('Пожалуйста, отправьте фото!')
await state.set_state(Form.photo)
@questionnaire_router.message(F.text, Form.about)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(about=message.text)
data = await state.get_data()
caption = f'Пожалуйста, проверьте все ли верно: \n\n' \
f'<b>Полное имя</b>: {data.get("full_name")}\n' \
f'<b>Пол</b>: {data.get("gender")}\n' \
f'<b>Возраст</b>: {data.get("age")} лет\n' \
f'<b>Логин в боте</b>: {data.get("user_login")}\n' \
f'<b>О себе</b>: {data.get("about")}'
await message.answer_photo(photo=data.get('photo'), caption=caption, reply_markup=check_data())
await state.set_state(Form.check_state)
# сохраняем данные
@questionnaire_router.callback_query(F.data == 'correct', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
await call.answer('Данные сохранены')
await call.message.edit_reply_markup(reply_markup=None)
await call.message.answer('Благодарю за регистрацию. Ваши данные успешно сохранены!')
await state.clear()
# запускаем анкету сначала
@questionnaire_router.callback_query(F.data == 'incorrect', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
await call.answer('Запускаем сценарий с начала')
await call.message.edit_reply_markup(reply_markup=None)
await call.message.answer('Привет. Для начала выбери свой пол: ', reply_markup=gender_kb())
await state.set_state(Form.gender)Обратите внимание на изменения в классе Form, добавлены новые состояния:
python
class Form(StatesGroup):
gender = State()
age = State()
full_name = State()
user_login = State()
photo = State()
about = State()
check_state = State()Теперь рассмотрим обработчики:
python
@questionnaire_router.message((F.text.lower().contains('мужчина')) | (F.text.lower().contains('женщина')), Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(gender=message.text)
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Супер! А теперь напиши сколько тебе полных лет: ', reply_markup=ReplyKeyboardRemove())
await state.set_state(Form.age)
@questionnaire_router.message(F.text, Form.gender)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(name=message.text)
async with ChatActionSender.typing(bot=bot, chat_id=message.chat.id):
await asyncio.sleep(2)
await message.answer('Пожалуйста, выбери вариант из тех что в клавиатуре: ', reply_markup=gender_kb())
await state.set_state(Form.gender)Тут в двух обработчиках указали Form.gender, но при этом в одном есть фильтры, которые позволяют идти дальше, а в другом их нет, и бот снова просит выбрать пол (подробно разбирали в теме магических фильтров почему так происходит).
Обратите внимание, что добавили в хранилище ещё и telegram_id пользователя под ключем user_id. Эта информация нам нужна будет для записи пользователя в базу данных.
Посмотрим на формат захвата логина пользователя.
python
text = 'Теперь укажите ваш логин, который будет использоваться в боте'
if message.from_user.username:
text += ' или нажмите на кнопку ниже и в этом случае вашим логином будет логин из вашего телеграмм: '
await message.answer(text, reply_markup=get_login_tg())
else:
text += ‘ : ‘
await message.answer(text)Тут выполнили проверку на наличие у пользователя логина в профиле Телеграм. Если логин есть, то бот дает возможность использовать его через клик по кнопке. Если нет, то варианта с выбором логина с профиля нет.
python
# вариант когда мы берем логин из профиля телеграмм
@questionnaire_router.callback_query(F.data, Form.user_login)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
await call.answer('Беру логин с телеграмм профиля')
await call.message.edit_reply_markup(reply_markup=None)
await state.update_data(user_login=call.from_user.username)
await call.message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
await state.set_state(Form.photo)
# вариант когда мы берем логин из введенного пользователем
@questionnaire_router.message(F.text, Form.user_login)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(user_login=message.from_user.username)
await message.answer('А теперь отправьте фото, которое будет использоваться в вашем профиле: ')
await state.set_state(Form.photo)Обратите внимание, что тут в callback_query я использовал:
python
await call.message.edit_reply_markup(reply_markup=None)Благодаря такой записи удалили инлайн клавиатуру после клика по ней.
Также обратите внимание на то, как я использовал фильтры. Отдельно обработаны ситуации работы с callback_query и с простым message.
По фото есть момент. Фотографии можно отправлять со сжатием (в таком случае бот будет видеть объект photo) или без сжатия (тогда фото будет отправлено как документ). В коде учтены все варианты, и вот что получилось:
python
@questionnaire_router.message(F.photo, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
photo_id = message.photo[-1].file_id
await state.update_data(photo=photo_id)
await message.answer('А теперь расскажите пару слов о себе: ')
await state.set_state(Form.about)
@questionnaire_router.message(F.document.mime_type.startswith('image/'), Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
photo_id = message.document.file_id
await state.update_data(photo=photo_id)
await message.answer('А теперь расскажите пару слов о себе: ')
await state.set_state(Form.about)
@questionnaire_router.message(F.document, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
await message.answer('Пожалуйста, отправьте фото!')
await state.set_state(Form.photo)Когда тип контента — photo, все понятно. А в варианте с фото без сжатия использовали особый магический фильтр:
python
F.document.mime_type.startswith(‘image/’)Он проверяет, является ли MIME-тип документа изображением. Если он начинается с image/, это показывает, что это изображение, и нам это подходит.
На случай, если был отправлен просто документ (например, pdf), прописали обработчик:
python
@questionnaire_router.message(F.document, Form.photo)
async def start_questionnaire_process(message: Message, state: FSMContext):
await message.answer(‘Пожалуйста, отправьте фото!’)
await state.set_state(Form.photo)Смысл в том, чтобы возвращать пользователя к отправке фото, если он прислал не то, что нужно.
Далее мы сохраняем описание о себе и получаем данные о пользователе из хранилища. Так как хотим отправить сообщение в формате анкеты, будем отвечать пользователю отправкой фото-сообщения.
python
@questionnaire_router.message(F.text, Form.about)
async def start_questionnaire_process(message: Message, state: FSMContext):
await state.update_data(about=message.text)
data = await state.get_data()
caption = f'Пожалуйста, проверьте все ли верно: \n\n' \
f'<b>Полное имя</b>: {data.get("full_name")}\n' \
f'<b>Пол</b>: {data.get("gender")}\n' \
f'<b>Возраст</b>: {data.get("age")} лет\n' \
f'<b>Логин в боте</b>: {data.get("user_login")}\n' \
f'<b>О себе</b>: {data.get("about")}'
await message.answer_photo(photo=data.get('photo'), caption=caption, reply_markup=check_data())
await state.set_state(Form.check_state)Далее, в зависимости от варианта проверки, либо сохраним данные о пользователе в базе данных (следующая статья), либо запустим сценарий сначала.
python
# сохраняем данные
@questionnaire_router.callback_query(F.data == 'correct', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
await call.answer('Данные сохранены')
await call.message.edit_reply_markup(reply_markup=None)
await call.message.answer('Благодарю за регистрацию. Ваши данные успешно сохранены!')
await state.clear()
# запускаем анкету сначала
@questionnaire_router.callback_query(F.data == 'incorrect', Form.check_state)
async def start_questionnaire_process(call: CallbackQuery, state: FSMContext):
await call.answer('Запускаем сценарий с начала')
await call.message.edit_reply_markup(reply_markup=None)
await call.message.answer('Привет. Для начала выбери свой пол: ', reply_markup=gender_kb())
await state.set_state(Form.gender)Вот как анкета выглядит в действии на скринах:


Разобрали фундаментальную тему, которая откроет вам путь к созданию ботов с невероятно сложными и увлекательными сценариями FSM. Понимание этого материала — ключ к созданию действительно умных и интерактивных ботов.