Skip to content

Aiogram 3: Инлайн кнопки и CallBack Дата

https://habr.com/ru/articles/820877/

Логичным продолжением будет изучение CallBack хендлеров и CallBack данных. Эти технологии открывают безграничные возможности для создания интерактивных и сложных сценариев взаимодействия с пользователями.

В данной статье мы рассмотрим:

  • Что такое CallBack хендлеры;

  • Разновидности CallBack хендлеров (ссылки, веб-приложения, обычные CallBack данные);

  • Научимся создавать более сложные конструкции через магические фильтры в контексте CallBack.

  • Поработаем с библиотекой Faker

  • Напишем функцию по генерации случайного пользователя и коснемся темы форматирования сообщений

  • Я покажу вам как работает имитация действий в боте (будем имитировать набор текста ботом)

Что такое CallBack в Aiogram 3

CallBack в Aiogram 3 — это способ обработки взаимодействий пользователей с ботом, когда они нажимают на инлайн кнопки. Когда пользователь нажимает на такую кнопку, бот получает специальное сообщение — CallBack, с информацией о том, какую кнопку нажали. Эта информация называется CallBack дата.

Проще говоря, CallBack позволяет боту реагировать на нажатия инлайн кнопок, выполняя определённые действия в ответ на это. Это очень удобно для создания интерактивных и динамических ботов, которые могут менять своё поведение в зависимости от выбора пользователя.

CallBack кнопки могут работать, как в формате текстовых кнопок. Это когда текст на кнопке равняется той информации (CallBack дате), которую пользователь передает боту. Бывает полезным, например, если в вашем боте нет текстовых клавиатур (в рамках общего стиля например), а есть только InlineKeyboard. В таком случае можно, к примеру, в инлайн кнопку Home передать CallData home, но обычно в практике обычно CallBack дата кардинально отличается от надписи на кнопке.

Сейчас я дам пару текстовых примеров, чтобы стало более понятно, а после приступим к написанию кода.

Например, у вас бот службы поддержки (порядка 5-6 проектов разной сложности делал в формате «support bot»). Пользователь отправляет некое сообщение в бота, бот перехватывает это сообщение, параллельно захватив телеграмм ID пользователя.

Далее он отправляет это сообщение менеджерам службы поддержки с инлайн кнопкой «Ответить». Менеджер нажимает на эту кнопку, и бот ждёт ответного сообщения (подробнее рассмотрим в теме FSM). И тут основная фишка в том, что кликнув на кнопку «Ответить», менеджер не просто запускает сценарий ответа на сообщение, но и сразу указывает боту, что этот ответ должен полететь конкретному пользователю. В данном случае бот достает телеграмм айди пользователя из CallData (сделаем нечто похожее).

Другой пример (тоже из службы поддержки):

В ботах периодически бывают так называемые FAQ (разделы с часто задаваемыми вопросами). Был опыт, когда я в админ панели прописывал функционал, позволяющий захватить вопрос и ответ на него (FSM), далее это всё записывалось в базу данных под определённым айдишником.

После, когда пользователь заходит в раздел FAQ, бот отправляет запрос в базу данных и при помощи генератора инлайн кнопок (InlineKeyboardBuilder по типу такого же как для текстовых кнопок) происходит генерация клавиатуры с вопросом и ответом.

Далее боту достаточно всего одного хендлера для того чтобы массово обрабатывать сразу все ответы на любые вопросы. Ниже пример реализации:

python
@qst_router.callback_query(F.data.startswith('qst_'))
async def cmd_start(call: CallbackQuery, state: FSMContext):
    await state.clear()
    await call.answer()
    qst_id = int(call.data.replace('qst_', ''))
    async with ChatActionSender(bot=bot, chat_id=call.from_user.id, action="typing"):
        info = await pg_db.select_data('questions', {'where_conditions': [{'id': qst_id}]})
        await call.message.answer(info.get('answer'), reply_markup=main_kb(call.from_user.id))

Сильно не вдавайтесь сейчас в подробности кода, в будущих статьях я вас научу делать каждую реализацию. Тут просто смысл в демонстрации мощи CallBackData — буквально пару строк кода способны закрыть огромный блок FAQ (масштабирование, по сути, неограниченное).

Если сейчас пока не понятно что к чему – не переживайте. Дочитав эту статью, вы точно разберётесь с темой CallBack.

Приступаем к коду.

Код я буду писать в том же проекте, что и писал в прошлых статьях (если хотите такой же шаблон как у меня – читайте первую статью по теме aiogram – там я подробно расписал свой каркас бота стартового).

Давайте в нашем пакете keyboards создадим новый файл под Inline клавиатуры и дадим ему название inline_kbs.py:

В него сразу импортируем следующие модули:

python
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
from aiogram.utils.keyboard import InlineKeyboardBuilder
  • InlineKeyboardMarkup: Этот класс используется для создания разметки инлайн клавиатуры. Разметка определяет, как будут располагаться кнопки на клавиатуре и как они будут взаимодействовать с пользователем.
  • InlineKeyboardButton: Этот класс представляет собой отдельную кнопку на инлайн клавиатуре. С помощью него мы можем задавать текст кнопки и действие, которое произойдет при нажатии на неё, например, отправку CallBack данных.
  • WebAppInfo: Этот класс используется для создания кнопок, которые открывают веб-приложения внутри Telegram С его помощью можно определить URL веб-приложения, которое будет открыто при нажатии на кнопку. Это полезно для интеграции внешних веб-сервисов и приложений с ботом.
  • InlineKeyboardBuilder: Это удобный инструмент для построения инлайн клавиатур. С его помощью можно легко и быстро создавать клавиатуры, добавляя кнопки и определяя их расположение. Этот класс помогает упрощать процесс создания сложных разметок клавиатур, делая код более читаемым и удобным для поддержки. Работает похожим образом с ReplyKeyboardBuilder, но со своими особенностями.

Инлайн клавиатура со ссылками

python
def ease_link_kb():
    inline_kb_list = [
        [InlineKeyboardButton(text="Мой хабр", url='https://habr.com/ru/users/yakvenalex/')],
        [InlineKeyboardButton(text="Мой Telegram", url='tg://resolve?domain=yakvenalexx')],
        [InlineKeyboardButton(text="Веб приложение", web_app=WebAppInfo(url="https://tg-promo-bot.ru/questions"))]
    ]
    return InlineKeyboardMarkup(inline_keyboard=inline_kb_list)
  • Функция ease_link_kb предназначена для создания и возвращения инлайн клавиатуры с кнопками, которые ведут к различным ссылкам различных типов.

  • Внутри функции создаётся список inline_kb_list, который содержит вложенные списки с объектами InlineKeyboardButton. Каждая вложенная структура представляет собой отдельную строку кнопок на инлайн клавиатуре.

  • [InlineKeyboardButton(text="Мой хабр", url='https://habr.com/ru/users/yakvenalex/')]: Создаётся кнопка с текстом "Мой хабр", которая при нажатии перенаправляет пользователя на страницу Хабра.

  • [InlineKeyboardButton(text="Мой Telegram", url='tg://resolve?domain=yakvenalexx')]: Создаётся кнопка с текстом "Мой Telegram", которая при нажатии открывает Telegram и переходит к моему аккаунту.

  • [InlineKeyboardButton(text="Веб приложение", web_app=WebAppInfo(url="https://tg-promo-bot.ru/questions"))]: Создаётся кнопка с текстом "Веб приложение", которая при нажатии открывает веб-приложение по указанному URL.

  • Возвращение инлайн клавиатуры: return InlineKeyboardMarkup(inline_keyboard=inline_kb_list)

Этот пример демонстрирует, как создавать инлайн клавиатуру с различными типами ссылок, включая обычные URL, ссылки на аккаунты в Telegram и веб-приложения. Давайте тестировать.

Для тестов я предлагаю создать новый message handler, который будет вызываться текстом «Давай инлайн!». К нему прикрутим нашу инлайн клавиатуру и поклацаем ее.

python
@start_router.message(F.text == 'Давай инлайн!')
async def get_inline_btn_link(message: Message):
    await message.answer('Вот тебе инлайн клавиатура со ссылками!', reply_markup=ease_link_kb())

Думаю, что к настоящему моменту вы уже понимаете, что тут мы использовали магический фильтр F.text, который будет срабатывать на отправку текста 'Давай инлайн!', а чтоб было ещё интересней давайте создадим текстовую кнопку с текстом 'Давай инлайн!', а саму кнопку привяжем к главной клавиатуре .

Так теперь выглядит главная клавиатура

img.png

Так выглядит инлайн клавиатура со ссылками

img_1.png

Давайте теперь изучать каждую кнопку-ссылку:

После клика на обычную кнопку-ссылку появляется окно которое спрашивает хотим ли мы перейти по ссылке.

img_2.png

После клика на ссылку с моим профилем Telegram происходит переход без окна в мой профиль. До недавнего времени только ссылки формата tg://resolve?domain=yakvenalexx позволяли переходить в профиль без окна, но, при написании этого текста обнаружил что и при формате ссылки https://t.me/yakvenalexx окно не появлялось.

Теперь к интересному моменту ВЕБ-ПРИЛОЖЕНИЕ:

После клика по инлайн кнопке с «квадратиком» телеграмм отправит нам такое сообщение:

img_3.png

Пример простого приложения

img_4.png

А после откроется то ВЕБ-ПРИЛОЖЕНИЕ, которое я создал. С ПК версии, возможно не так наглядно как с телефона. Сейчас продемонстрирую.

img_5.png

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

img_6.png

img_7.png

Итак, к промежуточным выводам. Мы разобрали все варианты инлайн-клавиатур ссылок, а это значит, что можем переходить к более интересной части – CallBack Data!

Начнем с простой клавиатуры. Пускай в ней будет 2 инлайн-кнопки. Одна кнопка должна переводить пользователя на стартовый экран, а вторая запускает некое действие, например, выводит информацию о случайном пользователе (первое, что пришло в голову).

Для этого нам нужно подготовиться.

Сначала напишем функцию, которая будет генерировать информацию о случайном пользователе. Для этого воспользуемся интересной библиотекой Faker (возьмите её на заметку, часто пригождается).

Устанавливаем библиотеку:

shell
pip install faker

Пишем функцию, которая будет генерировать случайного пользователя (как раз наш пакет с утилитами тут будет кстати).

python
from faker import Faker

def get_random_person():
    # Создаем объект Faker с русской локализацией
    fake = Faker('ru_RU')

    # Генерируем случайные данные пользователя
    user = {
        'name': fake.name(),
        'address': fake.address(),
        'email': fake.email(),
        'phone_number': fake.phone_number(),
        'birth_date': fake.date_of_birth(),
        'company': fake.company(),
        'job': fake.job()
    }
    return user

Импортируем модуль. При инициализации объекта fake укажем, что нас интересуют русские данные. Далее создадим простой словарь и захватим в него следующие данные: имя, адрес, email, телефон, дата рождения, компания и работа.

Далее мы напишем специальный хендлер, который при получении CallData get_person будет возвращать хорошо оформленное сообщение с информацией о пользователе (как раз немного углубим свои знания в теме форматирования текста в aiogram 3).

Сначала импортируем функцию для генерации случайного пользователя из пакета utils и CallbackQuery для удобства аннотаций.

python
from utils.utils import get_random_person
from aiogram.types import CallbackQuery

Теперь напишем сам хендлер. Я его пропишу полностью, а дальше дам объяснения.

python
@start_router.callback_query(F.data == 'get_person')
async def send_random_person(call: CallbackQuery):
    # await call.answer('Генерирую случайного пользователя')
    user = get_random_person()
    formatted_message = (
        f"👤 <b>Имя:</b> {user['name']}\n"
        f"🏠 <b>Адрес:</b> {user['address']}\n"
        f"📧 <b>Email:</b> {user['email']}\n"
        f"📞 <b>Телефон:</b> {user['phone_number']}\n"
        f"🎂 <b>Дата рождения:</b> {user['birth_date']}\n"
        f"🏢 <b>Компания:</b> {user['company']}\n"
        f"💼 <b>Должность:</b> {user['job']}\n"
    )
    await call.message.answer(formatted_message)

Тут у нас изменился декоратор. Теперь это не @start_router.message, а @start_router.callback_query. Также изменился магический фильтр. Теперь мы обрабатываем не F.text, а F.data.

Также мы указываем, что работать будем с объектом CallbackQuery, что позволит нам получать подсказки от IDE, в котором мы ведем разработку бота.

Обратите внимание, что я закомментировал одну строку. Это сделано намеренно, и далее вы поймете зачем.

После мы сгенерировали нашего пользователя через функцию, которую писали ранее, а затем приступили к простому форматированию текста. Если вы пользуетесь моей структурой бота, то у вас тоже при инициализации указывалось:

python
bot = Bot(token=config('TOKEN'), default=DefaultBotProperties(parse_mode=ParseMode.HTML))

Тем самым мы научили бота воспринимать HTML теги в сообщениях.

Форматирование у нас достаточно простое. Из тегов мы использовали только <b></b>, который делает текст жирным. Далее мы достаем из объекта user данные и при помощи \n делаем перенос на новую строку.

С помощью await call.message.answer(formatted_message) мы отправляем сообщение пользователю (не путать с await call.answer!).

Теперь напишем нашу инлайн-клавиатуру с call_data и приступим к тестированию.

python
def get_inline_kb():
    inline_kb_list = [
        [InlineKeyboardButton(text="Генерировать пользователя", callback_data='get_person')],
        [InlineKeyboardButton(text="На главную", callback_data='back_home')]
    ]
    return InlineKeyboardMarkup(inline_keyboard=inline_kb_list)

Вы можете обратить внимание, что клавиатура не особо отличается от клавиатуры со ссылками. Единственное отличие в том, что вместо url мы передаем callback_data, на которые и будет реагировать наш бот (обработчик под callback_data='get_person' мы уже написали).

Давайте эту клавиатуру привяжем вместо клавиатуры со ссылками. Отлично. Запускаем бота и смотрим, что у нас получилось:

img_8.png

Немного изменил текст в хендлере, раньше бот говори про ссылки

Мы видим, что информация о пользователе сгенерирована, но кнопка не перестает мигать (знаю, что на скрине это плохо видно и понятно, но, поверьте, она мигает, а на смартфоне будут часики возле кнопки). Дело в том, что инлайн-клавиатуры в Telegram устроены таким образом, что они всегда ждут ответа от хендлера, что тот выполнен. Ждут порядка 30 секунд, после чего успокаиваются.

Давайте мы дадим ответ серверам Telegram, что все ок и по плану:

python
await call.answer('Генерирую случайного пользователя', show_alert=False)

show_alert=False идет по умолчанию, но я оставлю это в коде, чтобы вас не путать. Перезапускаем бота и смотрим. Обратите внимание и на формат сообщения что мы получили.

img_9.png

Мы видим, что появилась белая надпись на черной плашке (висит секунды 2-3) и, при этом, у нас кнопка не мигает больше. Отлично.

Если вам нечего сообщать пользователю, можете просто указывать await call.answer(), тогда кнопка будет мгновенно тухнуть. Давайте сменим теперь флаг show_alert на True и посмотрим, что у нас получится:

img_10.png

Теперь у нас появляется окно alert, и для продолжения нужно будет нажать на «Ок». В некоторых случаях это бывает удобно.

Теперь, для закрепления материала, давайте пропишем хендлер для обработки callback_data='back_home'. Выполните самостоятельно, уверен, что у вас все получится.

Генератор инлайн-клавиатур - InlineKeyboardBuilder

Сейчас мы не просто разберем генератор инлайн-клавиатур, но и попробуем написать свой первый более-менее серьезный обработчик. Смысл всей затеи будет таким:

  1. Мы соберем некий массив данных (питоновский словарь наподобие JSON). Пусть это будут вопросы с ответами. Словарь будет иметь ключ в виде целого числа и принадлежащий ему массив данных в виде вопроса и ответа.

  2. Мы напишем функцию, которая будет генерировать инлайн-клавиатуры такого вида:

    • text = «Вопрос»
    • callback_data = f-строка, содержащая приставку qst_ и ID вопроса
  3. Универсальный хендлер, который будет давать ответ на каждый вопрос.

Я знаю, что уже давал выше пример такой функции из «боевого» бота. Тут же мы пропишем её упрощенную версию.

Начнем с массива вопросов и ответов. Напишем таких вопросов 10 штук. Вот пример:

python
questions = {
    1: {'qst': 'Столица Италии?', 'answer': 'Рим'},
    2: {'qst': 'Сколько континентов на Земле?', 'answer': 'Семь'},
    3: {'qst': 'Самая длинная река в мире?', 'answer': 'Нил'},
    4: {'qst': 'Какой элемент обозначается символом "O"?', 'answer': 'Кислород'},
    5: {'qst': 'Как зовут главного героя книги "Гарри Поттер"?', 'answer': 'Гарри Поттер'},
    6: {'qst': 'Сколько цветов в радуге?', 'answer': 'Семь'},
    7: {'qst': 'Какая планета третья от Солнца?', 'answer': 'Земля'},
    8: {'qst': 'Кто написал "Войну и мир"?', 'answer': 'Лев Толстой'},
    9: {'qst': 'Что такое H2O?', 'answer': 'Вода'},
    10: {'qst': 'Какой океан самый большой?', 'answer': 'Тихий океан'},
}

Этот массив я прописал в файле create_bot.py

Теперь напишем функцию, которая будет принимать словарь вопросов и возвращать инлайн-клавиатуру:

python
from aiogram.utils.keyboard import InlineKeyboardBuilder

def create_qst_inline_kb(questions: dict) -> InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()
    # Добавляем кнопки вопросов
    for question_id, question_data in questions.items():
        builder.row(
            InlineKeyboardButton(text=question_data.get('qst'), callback_data=f'qst_{question_id}')
        )
    # Добавляем кнопку "На главную"
    builder.row(
        InlineKeyboardButton(text='На главную', callback_data='back_home')
    )
    # Настраиваем размер клавиатуры
    builder.adjust(1)
    return builder.as_markup()

Функция генератор инлайн клавиатуры не сильно отличается от своего аналога в генерации текстовых клавиатур. Она принимает питоновский словарь с вопросами, а после пробегается по каждому генерируя нужные нам кнопки.

Обратите внимание, что в конец этой клавиатуры я добавил callback_data='back_home'(надеюсь, вы написали обработчик). Давайте теперь привяжем клавиатуру эту, например, к новому хендлеру, который будет реагировать на команду /faq (для закрепления данных из прошлых моих статей можете закрепить эту команду в командном меню).

python
async def set_commands():
    commands = [BotCommand(command='start', description='Старт'),
                BotCommand(command='start_2', description='Старт 2'),
                BotCommand(command='faq', description='Частые вопросы')]
    await bot.set_my_commands(commands, BotCommandScopeDefault())

Получился такой результат. Пишем обработчик.

python
 @start_router.message(Command('faq'))
async def cmd_start_2(message: Message):
    await message.answer('Сообщение с инлайн клавиатурой с вопросами', reply_markup=create_qst_inline_kb(questions))

Вот простой обработчик. Вопросы я импортировал из файла create_bot:

python
from create_bot import questions

Запускаем и смотрим что у нас получилось:

img_11.png

Видим, что вопросы благополучно подгрузились. Теперь нам остается написать универсальный обработчик, который будет отвечать на выбранный вопрос. Давайте к каждому ответу прикрутим инлайн клавиатуру с вопросами.

python
@start_router.callback_query(F.data.startswith('qst_'))
async def cmd_start(call: CallbackQuery):
    await call.answer()
    qst_id = int(call.data.replace('qst_', ''))
    qst_data = questions[qst_id]
    msg_text = f'Ответ на вопрос {qst_data.get("qst")}\n\n' \
               f'<b>{qst_data.get("answer")}</b>\n\n' \
               f'Выбери другой вопрос:'
    async with ChatActionSender(bot=bot, chat_id=call.from_user.id, action="typing"):
        await asyncio.sleep(2)
        await call.message.answer(msg_text, reply_markup=create_qst_inline_kb(questions))

Не волнуйтесь, сейчас со всем разберемся. Для начала выполним импорты:

python
import asyncio
from aiogram.utils.chat_action import ChatActionSender
from create_bot import questions, bot

asyncio нам тут нужен для одной цели – установим асинхронную паузу в 2 секунды. Сама пауза нам нужна для того чтоб мы могли имитировать набор ботом текста. Для этого мы использовали конструкцию async with ChatActionSender(bot=bot, chat_id=call.from_user.id, action="typing"). Сильно заострять тут внимание не буду, но общий смысл в том, что бот, в течении 2 секунд, имитирует набор текста.

Давайте к разбору кода.

F.data.startswith('qst_')) – нововведения начались с этой части. Указанная конструкция выполнила проверку на то начинается ли CallBack data с qst_ (магические фильтры в деле).

Далее, строкой await call.answer() мы дали понять серверу телеграмм, что все у нас хорошо и все по плану (кнопка потухнет сразу).

А вот на этой строке qst_id = int(call.data.replace('qst_', '')) немного заострим внимание, так как на этом трюке можно выстраивать невероятно сложные сценарии интерактивного взаимодействия пользователя и телеграмм бота.

Данным трюком мы забираем значение call_data и трансформируем строку в id вопроса, тем самым, давая боту понять ответ на какой вопрос мы хотим получить. Технически call.data – это самая обыкновенная строка, а значит с ней можно делать все что с обычными строками.

То есть в одной call_data вы можете передать много информации, например ID пользователя, сумма оплаты и идентификатор товара, который пользователь покупает в вашем боте.

Выглядит так: order_112344_232_1245

Далее специальный хендлер срабатывает на F.data.startswith(order_')), а далее из строки забирает нужные ему данные. Представляете какие это возможности открывает?

Далее мы достаем нужный нам вопрос, через его id, а после просто формируем сообщение, которое бот отправит пользователю через f-строку, забирая нужные нам данные.

Ну а дальше сделали имитацию набора текста на 2 секунды и затем отправили сообщение с клавиатурой. Смотрим на примере:

img_12.png

Имитация набора текста 2 секунды

Как вы видите бот начал имитировать набор текста. А вот и ответ:

img_13.png

Contacts: teffal@mail.ru