Повествование о разработке бота Work for everyone продолжаю с описания хендлеров, отвечающих за обработку команд /start и /help.
Кроме того, сразу же напишем хендлер, который будет отвечать на сообщения, обработка которых пока еще не реализована и не будет реализована в будущем. Это нужно для того, чтобы пользователь, который случайно отправил не то, что от него ожидалось, смог это понять и имел возможность повторить действие и получить справку по работе с ботом.
Что такое хендлер?
Хендлер – это такой инструмент, который позволяет работать с потоком сообщений от пользователя, анализирует их и отбирает нужные. Сами сообщения можно представить как очередь. С точки зрения Python и фреймворка AIOgram, хендлер — это просто декорированная асинхронная функция, которая отлавливает с помощью декоратора нужное сообщение и совершает заданные действия (направляет ответ на сообщение, осуществляет запись данных в БД, отправляет запрос к стороннему API и т.д.).
Подготовка основы для работы хендлеров
Прежде, чем я перейду к описанию хендлеров, необходимо проговорить, что нам понадобится для их работы.
Регистрация хендлеров
Для того чтобы хендлеры, они же функции-обработчики событий, заработали, их необходимо подключить к диспетчеру бота (зарегистрировать). Делать я буду это через роутеры.
Объект роутера создаем в каждом из модулей, в которых будут находиться наши обработчики. У меня их два: applicant_handlers.py и other_handlers.py:
router = Router(name=__name__)
Все хендлеры далее будем привязывать к созданному объекту роутера. Сами роутеры следует зарегистрировать в диспетчере бота.
В основном исполняемом файле bot.py, который расположен в пакете бота (bot), в функции main(), отвечающей за асинхронный запуск бота, мы создаем объект диспетчера:
dp = Dispatcher(storage=config.tg_bot.storage)
А затем через функцию include_routers регистрируем наши роутеры:
dp.include_routers(applicant_handlers.router, other_handlers.router)
Текст сообщений для хендлеров
В первую очередь, необходим текст сообщений, который будет видеть пользователь при активации команд /start и /help, да и собственно других команд бота. Писать такой текст в самом хендлере не самая лучшая практика.
Бот Work for everyone будет строиться на основе модулей, каждый из которых будет отвечать за определенный его функционал. Одним из таких модулей будет модуль, в котором,в виде словаря будет храниться текст сообщений.
Применительно к моему боту тексты всех сообщений находятся в модуле phrases_for_bot_messages.py, расположенном в директории phrases.
В указанном модуле я создал словарь PHRASES_FOR_MESSAGE, в котором в качестве ключей используется текст типа ‘start_command’ или ‘help_command’, а в качестве значения — непосредственно текст сообщения. Такие имена ключей, на мой взгляд, достаточно информативны.
В хендлерах я буду получать значение из словаря по соответствующему ключу методом get():
PHRASES_FOR_MESSAGE.get('start_command') PHRASES_FOR_MESSAGE.get('help_command')
Тексты сообщений я здесь приводить не буду. В GitHub репозитории проекта вы всегда их можете посмотреть, при желании.
Кнопки «Готов!» и «Справка по боту»
Согласно собственноручно написанному техническому заданию, при получении команды /start бот должен направить пользователю приветственное сообщение и две кнопки: «Готов!» и «Справка по боту».
Кроме того, при активации команды /help или же при нажатии кнопки «Справка по боту» пользователь, кроме текста справки, получает и кнопку «Начать ввод данных».
Активация кнопок «Готов!» и «Начать ввод данных» влечет одинаковые последствия – запуск основной логики работы бота.
В первоначальном коде я реализовал функционал с помощью «стандартных» кнопок, которые появляются под полем для ввода сообщений. По своей сути «стандартные» кнопки представляют собой обычное текстовое сообщение, отправляемое пользователем. Такое решение мной было принято из-за того, что данный вид кнопок являются доступными для программ экранного доступа, а inline кнопки нет…
Вот я дурень! Оказалось, что inline кнопки вполне доступны для скринридеров. Так что дальше я буду работать с данным видом кнопок. Правда, для этого надо переделать уже готовую часть кода, но, блин, оно того стоит — возможности таких кнопок намного шире «стандартных».
Так же, как и для текста сообщений, я добавил в бот модуль texts_for_bot_buttons.py, в котором и буду в словаре TEXT_FOR_BUTTON хранить текст кнопок.
Клавиатуры буду создавать самым примитивным способом, через билдер. Реализовывать на этом этапе какие-либо функции с большей абстракцией с возможностью повторного применения, пока не вижу необходимости.
В пакете bot создаю директорию keyboards, а уже в ней файл keyboards.py, где и буду держать все клавиатуры, которые понадобятся при работе бота. Дальше все просто. Покажу процесс на примере клавиатуры, которую бот показывает пользователю при получении команды /start.
Создаем кнопки с помощью класса InlineKeyboardButton:
bt_ready = InlineKeyboardButton( text=TEXT_FOR_BUTTON.get('ready'), callback_data='ready' ) bt_help = InlineKeyboardButton( text=TEXT_FOR_BUTTON.get('bot_help'), callback_data='bot_help'
Далее создаю билдер и методом row() добавляю две кнопки, а также указываю ширину клавиатуры:
kb_start = InlineKeyboardBuilder() kb_start.row(bt_ready, bt_help, width=2)
В результате, после добавления клавиатуры в хендлер получаем вот такую клавиатуру:
Клавиатура полностью доступна для программ экранного доступа как в клиенте Telegram на Android, так и в клиенте Unigram на Windows 10.
Пример работы с inline кнопками на Android со скринридером TalkBack 14.1
Пример работы с inline кнопками на Windows 10 (клиент Unigram 10.4, программа экранного доступа NVDA 2022.3.3)
Как видно, все кнопки адекватно озвучиваются.
Код хендлеров
Заготовки текстовых сообщений для отправки пользователям есть, кнопки и клавиатуры создал, роутеры зарегистрировал в диспетчере, пришла пора показать код получившихся хендлеров.
В боте пока есть два вида хендлеров. Одни хендлеры обрабатывают команды, пока это команды /start и /help, другие — обрабатывают нажатие inline кнопок. Приведу пример каждого из них.
Обработка команды /start
@router.message(CommandStart()) async def start_command_processing(message: Message): """Хендлер, срабатывающий на команду /start.""" text = ( f'{message.from_user.first_name}, ' f'{PHRASES_FOR_MESSAGE.get('start_command')}' ) await message.answer( text=text, reply_markup=kb_start.as_markup(), ) await message.delete()
Обработка кнопки «bot_help»
@router.callback_query(F.data == 'bot_help') async def bot_help_button_processing(callback: CallbackQuery): """Хендлер, срабатывающий на кнопку 'bot_help'.""" text = ( f'{callback.message.chat.first_name}, ' f'{PHRASES_FOR_MESSAGE.get("help_command")}' ) await callback.message.edit_text( text=text, reply_markup=kb_data_input.as_markup() )
Принципиально чего-то из ряда вон выходящего в них нет. Максимально простые функции и обрабатываемые действия. Да и зачем усложнять на этом месте.
Хотя есть одна проблема, которую я пока не решил. У меня есть два хендлера, которые практически идентичны. Один хендлер обрабатывает команду /help, другой — нажатие inline кнопки «Справка по боту». Пытался их объединить, но пока не получилось. Дело в том, что команда /help ловится как текстовое сообщение пользователя, а при нажатии кнопки необходимо работать с объектом класса CallbackQuery. В общем, еще подумаю над этим вопросом.
Что дальше?
Дальше, больше и интереснее! Необходимо начать реализацию работы основной логики бота. После того, как пользователь нажмет кнопку «Готов!» или кнопку «Начать ввод данных», бот должен предложить пользователю выбрать из списка федеральный округ, а затем и регион.
Список федеральных округов небольшой, он будет храниться в виде словаря, в котором в качестве ключей будет выступать официальный номер федерального округа, а в качестве значения – его наименование.
federal_districts = {30: 'Центральный федеральный округ'}
После того, как пользователь выберет нужный федеральный округ, бот отправит ему список регионов, которые входят в выбранный федеральный округ. Список регионов буду хранить в базе данных. На время разработки я буду использовать SQLite, а потом перенесу все на PostgreSQL.
При выборе регионов у пользователя должна быть реализована возможность вернуться обратно к выбору федерального округа, на случай неверного выбора.
Для корректной и последовательной работы нужно будет настроить машину состояний и фильтры. Фильтры будут свои.
Данные пользователя надо где-то хранить до окончания их полного ввода и валидации, и только после того, как пользователь подтвердит верность введенных данных, я буду делать запись в БД. Для этого в продакшн буду использовать Redis, а пока ограничусь «памятью из коробки» — FSM storage.
Заключение
На этом данную публикацию считаю можно завершать. Все публикации, в которых я рассказываю о ходе разработки Telegram бота, вы можете посмотреть в рубрике Telegram bot.