Урок 3. Машина состояний и то самое логгирование
Важно! Не забывайте обновлять библиотеку командой python3.6 -m pip install -U aiogram , так как разработчик постоянно обновляет её, избавляя от различных багов. Например в последнем релизе исправлены ошибки Fatal Python error: PyImport_GetModuleDict: no module dictionary! и передача списков в фильтр состояний.
Урок проводится с использованием aiogram версии 1.2
Сегодня мы научимся использовать:
Традиционно код урока доступен на GitHub
Создаем состояния
Так как мы сейчас будем разбирать машину состояний, эти самые состояния необходимо сначала создать. Сперва в голову приходит использование енумов, однако я предпочел воспользоваться встроенным классом Helper, который подходит для данной задачи даже лучше.
Итак, запишем в файл utils.py наш демонстрационный класс с состояниями:
from aiogram.utils.helper import Helper, HelperMode, ListItem class TestStates(Helper): mode = HelperMode.snake_case TEST_STATE_0 = ListItem() TEST_STATE_1 = ListItem() TEST_STATE_2 = ListItem() TEST_STATE_3 = ListItem() TEST_STATE_4 = ListItem() TEST_STATE_5 = ListItem()
Обращу внимание читателя на то, что здесь нам интересно использовать именно ListItem , а не Item , так как в таком случае мы сможем использовать сложение разных состояний для передачи в handler (об этом дальше).
Если вы ещё не знакомы с классом Helper , то советую позже ознакомиться, а сейчас можно просто посмотреть на значения всех элементов (или каждого по отдельности, если захотите):
print(TestStates.all()) # ['test_state_0', 'test_state_1', 'test_state_2', 'test_state_3', 'test_state_4', 'test_state_5']
Так как в этот раз нам придется отправлять много сообщений, для разнообразия вынесем их в messages.py — для красоты.
Ещё не забываем добавить в config.py токен своего бота и мы готовы писать логику!
Указываем хранилище состояний и включаем логгирование
К привычным с прошлых уроков импортам у нас добавляется ещё парочка, а именно:
from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.contrib.middlewares.logging import LoggingMiddleware
dp = Dispatcher(bot, storage=MemoryStorage()) dp.middleware.setup(LoggingMiddleware())
На первой строчке мы указали хранилище состояний в оперативной памяти, так как потеря этих состояний нам не страшна (да и этот вариант больше всего подходит для демонстрационных целей, так как не требует настройки). Однако если у вас от состояний что-то зависит, рекомендуется ипользовать более надеждное хранилище. На данный момент можно подключить Redis и RethinkDB.
На второй строчке подключаем логгирование. На нем долго останавливаться не буду — лучше проверьте его работу самостоятельно: например, можно добавить хэндлер только на текстовые сообщения, тогда при отправке стикера, фото (и т.п.), поста в канал, где бот является администратором, в логах увидите, что эти апдейты не отработаны никаким хэндлером.
Обрабатываем входящие сообщения
Итак, по традиции добавляем обработчики команд start и help :
@dp.message_handler(commands=['start']) async def process_start_command(message: types.Message): await message.reply(MESSAGES['start']) @dp.message_handler(commands=['help']) async def process_help_command(message: types.Message): await message.reply(MESSAGES['help'])
А так же «ловим» все сообщения, отправленные при «нулевом» состоянии:
@dp.message_handler() async def echo_message(msg: types.Message): await bot.send_message(msg.from_user.id, msg.text)
Переходим к главной теме нашего урока: состояниям
Эти самые состояния нужно как-то устанавливать, поэтому сделаем так:
@dp.message_handler(state='*', commands=['setstate']) async def process_setstate_command(message: types.Message): argument = message.get_args() state = dp.current_state(user=message.from_user.id) if not argument: await state.reset_state() return await message.reply(MESSAGES['state_reset']) if (not argument.isdigit()) or (not int(argument) < len(TestStates.all())): return await message.reply(MESSAGES['invalid_key'].format(key=argument)) await state.set_state(TestStates.all()[int(argument)]) await message.reply(MESSAGES['state_change'], reply=False)
Не забываем, что хэндлеры обрабатываются в порядке их расположения в коде, поэтому описанная выше функция должна идти раньше приведенных ниже обработчиков.
В функции process_setstate_command мы проверяем, идут ли с командой какие-то аргументы и запрашиваем текущее состояние пользователя. Если никаких аргументов с командой не идёт, сбрасываем состояние. Если же аргументы есть, то проверяем, чтобы те соответствовали нашему условию: не отрицательное число (минус в строке не пройдет валидацию .isdigit() ), которое меньше количества всех состояний. Если аргумент не подходит, сообщаем пользователю об этом. В ином случае устанавливаем новое текущее состояние и даем фидбек на действие сообщением. Обращу внимание читателя также на конструкцию message.reply(‘Some text’, reply=False) . Указав reply=False мы используем шорткат, позволяющий нам ответить в тот же чат, не доставая айди пользователя / чата как, например, вот тут.
Теперь отрабатываем входящие сообщения при выбранном состоянии
@dp.message_handler(state=TestStates.TEST_STATE_1) async def first_test_state_case_met(message: types.Message): await message.reply('Первый!', reply=False)
В данном хэндлере мы передаем в состояние TestStates.TEST_STATE_1 , что эквивалентно [‘test_state_1’] — отмечу дважды, тут передается именно массив.
Теперь добавим такой хэндлер:
@dp.message_handler(state=TestStates.TEST_STATE_2[0]) async def second_test_state_case_met(message: types.Message): await message.reply('Второй!', reply=False)
Здесь мы уже указываем состояние ‘test_state_2’ .
Библиотека сама понимает, когда мы передаем список состояний, а когда только одно состояние и под капотом обрабатывает их по-разному, но нам нет смысла об этом задумываться. В этом плане мы в плюсе, так как можем сделать вот так:
@dp.message_handler(state=TestStates.TEST_STATE_3 | TestStates.TEST_STATE_4) async def third_or_fourth_test_state_case_met(message: types.Message): await message.reply('Третий или четвертый!', reply=False)
Выражение TestStates.TEST_STATE_3 | TestStates.TEST_STATE_4 возвращает массив: [‘test_state_3’, ‘test_state_4’] . Грубо говоря, при проверке состояний библиотека проходится по списку и если встречает совпадение, хэндлер отрабатывается.
Ну и последний на сегодня хэндлер:
@dp.message_handler(state=TestStates.all()) async def some_test_state_case_met(message: types.Message): with dp.current_state(user=message.from_user.id) as state: text = MESSAGES['current_state'].format( current_state=await state.get_state(), states=TestStates.all() ) await message.reply(text, reply=False)
Он принимает в себя сообщения при всех состояниях из возможных в этом уроке, однако так как состояния с первого по четвертый ловятся хэндлерами выше, в этот попадают только нулевое и пятое.
Ещё в приведённом выше блоке кода используется контекстный менеджер ( with as ) — многим с ним удобнее.
Для красоты ещё стоит закрывать соединение с хранилищем состояний, для этого объявляем функцию:
async def shutdown(dispatcher: Dispatcher): await dispatcher.storage.close() await dispatcher.storage.wait_closed()
if __name__ == '__main__': executor.start_polling(dp, on_shutdown=shutdown)