Очень часто при написании определённого функционала, определённой цепочки действий, возникает необходимость как-то давать указания боту на что реагировать, а что пропускать мимо ушей. Что сохранять и как всё это совмещать. Всех приветствую дорогие форумчане! Сегодня мы поговорим про FSM (Finite State Machine), про машину состояний. Что это и как ей управляться. Давайте начнём. Представим ситуацию. Вам нужно сделать бота для онлайн покупок. Вот вы написали функционал каталога, пришло время сделать самое основное. Функцию заказа. Вот пользователь вводит название товара, вы его сохраняется в переменную (не дай бог глобальную), далее вы предлагаете пользователю ввести свой адрес. И тут на тебе. А как? Как сделать так, чтобы после того как пользователь ввёл свой адрес, выполнился определённый handler? Тут к нам на помощь приходит FSM (Finite State Machine). Как вы могли понять, данная модель помогает передвигаться в определенные функции по скрипту, а также сохранять данные определённого пользователя, у которого активно FSM состояние. Чтобы начать работать с машиной состояний, для начала нам нужно инициализировать хранилище в dp (Dispatcher) from aiogram.contrib.fsm_storage.memory import MemoryStorage storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) Python from aiogram.contrib.fsm_storage.memory import MemoryStorage storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) Отлично! Теперь мы можем записывать данные пользователя в оперативную память. Теперь давайте поговорим про состояния. Перед тем как создать какое-либо состояние, нам нужно создать класс, где мы поочередно опишем все states чтобы потом без проблем переключаться между ними. Возьмём пример с регистрацией пользователя и вводом его адреса. Создадим класс UserState. from aiogram.dispatcher.filters.state import StatesGroup, State class UserState(StatesGroup): name = State() address = State() Python from aiogram.dispatcher.filters.state import StatesGroup, State class UserState(StatesGroup): name = State() address = State() Тут мы импортируем StatesGroup и State. После создания класса, мы можем приступать к самому интересному, к основному написанию скрипта. Так что же будет делать бот? 1. спрашивать пользователя его имя 2. записывать имя в переменную состояния 3. переключаться на следующее состояние 4. спрашивать пользователя его адрес 5. записывать адрес в переменную состояния 6. выводить полученную информацию о пользователе 7. очищать состояние пользователя, а также все данные Для начала создадим handler, который будет обрабатывать команду /reg: @dp.message_handler(commands=['reg']) async def user_register(message: types.Message): await message.answer("Введите своё имя") await UserState.name.set() Python @dp.message_handler(commands=['reg']) async def user_register(message: types.Message): await message.answer("Введите своё имя") await UserState.name.set() Тут у нас встречается подобная строчка await UserState.name.set() Мы обращаемся к классу UserState, далее к состоянию name, а методом set() мы устанавливаем данное состояние. Теперь у нас пользователь имеет состояние UserState.name и мы можем создать handler, который будет реагировать ТОЛЬКО на это состояние. Давайте же создадим такой handler в который пользователь будет записывать своё имя: @dp.message_handler(state=UserState.name) async def get_username(message: types.Message, state: FSMContext): await state.update_data(username=message.text) await message.answer("Отлично! Теперь введите ваш адрес.") await UserState.next() # либо же UserState.address.set() Python @dp.message_handler(state=UserState.name) async def get_username(message: types.Message, state: FSMContext): await state.update_data(username=message.text) await message.answer("Отлично! Теперь введите ваш адрес.") await UserState.next() # либо же UserState.address.set() Итак, давайте разбираться: async def get_username(message: types.Message, state: FSMContext): Тут мы вторым аргументом передали state: FSMContext для того, чтобы мы могли записывать данные пользователя в память. Нам также необходимо импортировать: from aiogram.dispatcher import FSMContext Далее: await state.update_data(username=message.text) Здесь мы вызываем метод update_data(), который сохраняет данные в память, где единственным аргументом мы указываем название ключа в памяти 'username', а также его значение 'message.text'. Далее просим пользователя ввести его адрес, а после, неявно переключаем состояние на следующее (а следующий state у нас 'adress') с помощью метода await UserState.next() Однако переключайте состояния таким способом только тогда, когда точно знаете какой state (в классе) идёт дальше. Также можно переключать состояния явно: await UserState.address.set() Отлично Теперь создадим финальный хэндлер где будем получать адрес пользователя, а также выводить ему всю имеющуюся информацию. @dp.message_handler(state=UserState.address) async def get_address(message: types.Message, state: FSMContext): await state.update_data(address=message.text) data = await state.get_data() await message.answer(f"Имя: {data['username']}\n" f"Адрес: {data['address']}") await state.finish() Python @dp.message_handler(state=UserState.address) async def get_address(message: types.Message, state: FSMContext): await state.update_data(address=message.text) data = await state.get_data() await message.answer(f"Имя: {data['username']}\n" f"Адрес: {data['address']}") await state.finish() В начале в принципе мы с вами уже всё разобрали, state, FSMContext, update_data. Но тут у нас появляется новый актер. data = await state.get_data() С помощью метода get_data() мы получаем все ранее записанные в состояние данные, и сохраняем всё в переменную data. Теперь мы можем получить данные состояний по ключу через переменную data. Что.. собственно мы и делаем в следующей строчке при выводе всей информации пользователю. data = await state.get_data() await message.answer(f"Имя: {data['username']}\n" f"Адрес: {data['address']}") Python data = await state.get_data() await message.answer(f"Имя: {data['username']}\n" f"Адрес: {data['address']}") И в конце концов мы всё завершаем методом finish() await state.finish() Данный метод очищает все состояния пользователя, а также удаляет все ранее сохраненные данные. Если же вам надо сбросить только состояние, воспользуйтесь: await state.reset_state(with_data=False) Весь код: from aiogram import types from aiogram.dispatcher import FSMContext from aiogram.dispatcher.filters.state import State, StatesGroup class UserState(StatesGroup): name = State() address = State() @dp.message_handler(commands=['reg']) async def user_register(message: types.Message): await message.answer("Введите своё имя") await UserState.name.set() @dp.message_handler(state=UserState.name) async def get_username(message: types.Message, state: FSMContext): await state.update_data(username=message.text) await message.answer("Отлично! Теперь введите ваш адрес.") await UserState.next() # либо же UserState.adress.set() @dp.message_handler(state=UserState.address) async def get_address(message: types.Message, state: FSMContext): await state.update_data(address=message.text) data = await state.get_data() await message.answer(f"Имя: {data['username']}\n" f"Адрес: {data['address']}") await state.finish() Python from aiogram import types from aiogram.dispatcher import FSMContext from aiogram.dispatcher.filters.state import State, StatesGroup class UserState(StatesGroup): name = State() address = State() @dp.message_handler(commands=['reg']) async def user_register(message: types.Message): await message.answer("Введите своё имя") await UserState.name.set() @dp.message_handler(state=UserState.name) async def get_username(message: types.Message, state: FSMContext): await state.update_data(username=message.text) await message.answer("Отлично! Теперь введите ваш адрес.") await UserState.next() # либо же UserState.adress.set() @dp.message_handler(state=UserState.address) async def get_address(message: types.Message, state: FSMContext): await state.update_data(address=message.text) data = await state.get_data() await message.answer(f"Имя: {data['username']}\n" f"Адрес: {data['address']}") await state.finish() Итог Друзья, надеюсь всем всё понятно разъяснил, если понравилась статья, дайте об этом знать На этом всё, всем удачи и светлого ума! До скорого.
лютый смак The post was merged to previous Jun 2, 2022 try: await message.answer(text) await state.finish() <-- это не останавливает принятие инфы except AttributeError: await message.answer("Что-то пошло не так! \n" "Попробуйте еще раз") await state.finish() <-- а тут все работает, как надо как заставить работать первый state.finish()?
Давай статью про aiogram 3.0, там немного поменяли все, поэтому людям будет полезно. Не все знают английский и могут прочитать документацию, так что написание статьи имеет смысл. https://docs.aiogram.dev/en/dev-3.x/dispatcher/finite_state_machine/index.html
ucveoz, в хендлере пропиши фильтр content_types=[“document”] либо content_types=types.ContentType.DOCUMENT
Я учу щас aiogram. Уроки проходят по модульной системе, то есть всё по разным файлам. Вот я по-твоему уроку сделал файл reg.py, в котором описана логика регистрации пользователя. Автор уроков сначала импортирует dp из основного файла в наш (reg.py), потом уже в __init__.py импортирует dp из reg.py. Я поверхностно понял, что мы просто получаем все dp из reg.py. Ну так вот, при запуске бота, когда я ввожу команду /reg, как и задумано, бот спрашивает моё имя, но как только я ввожу его, начинает работать другой файл, отвечающий за функцию echo, то есть бот просто отвечает моим именем. В имортах __init__.py echo файл должен импортироваться последним, чтобы не перебивать другие (как я понял), но при этом он получается все равно его перебивает. Может знаешь решение ?
так можно же просто создать функцию с хендлером, которая будет запрашивать адрес, вызываться она будет после запроса имени, в чем тогда прикол FSM, может я чего то не понимаю?
yasnouguaga, так тебе же надо сохранить это состояние и передать дальше, типо если ты просто через хендлер это сделаешь, это никуда дальше не передается, а просто отображается как 1 сообщение
Статья выглядит годной, потому что тот же Груша расписал все куда заморочнее, чуть позже по твоему туториалу попробую сделать нужные себе функции и напишу получилось или нет
UPD: да, достаточно легко получилось, спасибо за статью. используя эту инфу и инфу из других источников можно запилить что требуется. Хотя конечно такой вопрос, я получил всю нужную инфу, она хранится в оперативке, а что с ней дальше делать? Могу ли я например попросить бота отправить всю эту информацию себе на почту, например? или на любую другую почту, которую я укажу в боте
Спасибо большое за данную статью. Долго ломал голову с python-telegram-bot, а в aiogram все оказалось проще.
Прикольно. Но чем это лучше обычного словаря типа states = { 'tg_id': {...user state...} }? Все равно все поля определять надо. Только непонятно, зачем брать и разбирать решение от aiogram, если придумать собственное займет 2 минуты и его можно сделать гораздо функциональней.
Xliteee, Просто запиши сам вместо пользователя @dp.message_handler(state=UserState.name) async def get_username(message: types.Message, state: FSMContext): await state.update_data(username='ВАСЯ') Python @dp.message_handler(state=UserState.name) async def get_username(message: types.Message, state: FSMContext): await state.update_data(username='ВАСЯ')
а вот как мне при команде админ, проверять айди пользователя и если оно верно писать ему какой либо текст
FarCatch, смотри, в типе Message есть свойство from_user.id (айди юзера который отправил месседж) и сделай проверку
Привет. Написал опрос на основе этой машины. Но появилась небольшая проблема. Сделал запуск машины с помощью инлайн клавиатуры, всё работает нормально. Но первый запрос приходит как уведомление. Т есть сверху экрана и тут же исчезает. Делаешь ввод отправляешь,то следующее сообщение приходит по нормальному и машина выполняется как положено. Не могу найти ответ почему сообщение приходит сверху. Help!
buruht32, msg: types.CallbackQuery msg.message.answer - отправит сообщение msg.answer - отправит что то тип уведомления
DEvgeniy, Спасибо,проблема уже решена. Правда другим способом. Пришлось добавить меню команд через BotFather,и теперь всё нормально отображается. А потом я уже нашёл ответ в каком то из видосов на Ютубе. Всё равно огромное спасибо что нашли время ответить на мой вопрос. Нюансов очень много и иногда приходится перелопатить много бесполезной инфы прежде чем отыщешь нужную.