Перейти к содержанию

Telegram bot: обработка команд start и help. Inline кнопки

Повествование о разработке бота 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)

В результате, после добавления клавиатуры в хендлер получаем вот такую клавиатуру:

Скриншот с Unigram. На скриншоте показано сообщение бота при старте, а также две кнопки: "Готов!" и "Справка по боту"

Клавиатура полностью доступна для программ экранного доступа как в клиенте 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.