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 redis
redis
— для взаимодействия с базой данных 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
. Понимание этого материала — ключ к созданию действительно умных и интерактивных ботов.