Приветствую всех! В этой статье мы продолжим исследовать возможности библиотеки Aiogram 3 и рассмотрим тему инлайн кнопок и CallBack данных. На данный момент мы уже:
Настроили нашего Telegram-бота на базе Aiogram 3;
Разобрались с командами, включая аргументы, командное меню и фильтры Command и CommandStart;
Освоили работу с текстовыми кнопками (в предыдущей публикации я максимально подробно разобрал эту тему);
Коснулись магических фильтров и обсудили прочие аспекты взаимодействия с ботом.
После тщательных размышлений я пришёл к выводу, что логичным продолжением будет изучение CallBack хендлеров и CallBack данных. Эти технологии открывают безграничные возможности для создания интерактивных и сложных сценариев взаимодействия с пользователями.
В данной статье мы рассмотрим:
Что такое CallBack хендлеры;
Разновидности CallBack хендлеров (ссылки, веб-приложения, обычные CallBack данные);
Научимся создавать более сложные конструкции через магические фильтры в контексте CallBack.
Поработаем с библиотекой Faker
Напишем функцию по генерации случайного пользователя и коснемся темы форматирования сообщений
Я покажу вам как работает имитация действий в боте (будем имитировать набор текста ботом)
Приступим к углубленному изучению этих аспектов и постараемся максимально использовать возможности, которые предоставляет нам библиотека Aiogram 3.
Что такое CallBack в Aiogram 3
CallBack в Aiogram 3 — это способ обработки взаимодействий пользователей с ботом, когда они нажимают на инлайн кнопки. Когда пользователь нажимает на такую кнопку, бот получает специальное сообщение — CallBack, с информацией о том, какую кнопку нажали. Эта информация называется CallBack дата.
Проще говоря, CallBack позволяет боту реагировать на нажатия инлайн кнопок, выполняя определённые действия в ответ на это. Это очень удобно для создания интерактивных и динамических ботов, которые могут менять своё поведение в зависимости от выбора пользователя.
CallBack кнопки могут работать, как в формате текстовых кнопок. Это когда текст на кнопке равняется той информации (CallBack дате), которую пользователь передает боту. Бывает полезным, например, если в вашем боте нет текстовых клавиатур (в рамках общего стиля например), а есть только InlineKeyboard. В таком случае можно, к примеру, в инлайн кнопку Home передать CallData home, но обычно в практике обычно CallBack дата кардинально отличается от надписи на кнопке.
Сейчас я дам пару текстовых примеров из моей реальной практики, чтобы стало более понятно, а после приступим к написанию кода.
Например, у вас бот службы поддержки (порядка 5-6 проектов разной сложности делал в формате «support bot»). Пользователь отправляет некое сообщение в бота, бот перехватывает это сообщение, параллельно захватив телеграмм айди пользователя (в прошлой статье я показывал как достать телеграмм айди из объекта message).
Далее он отправляет это сообщение менеджерам службы поддержки с инлайн кнопкой «Ответить». Менеджер нажимает на эту кнопку, и бот ждёт ответного сообщения (подробнее рассмотрим в теме FSM). И тут основная фишка в том, что кликнув на кнопку «Ответить», менеджер не просто запускает сценарий ответа на сообщение, но и сразу указывает боту, что этот ответ должен полететь конкретному пользователю (хотите статью о том, как написать простую службу поддержки?). В данном случае бот достает телеграмм айди пользователя из CallData (сегодня мы сделаем нечто похожее).
Другой пример (тоже из службы поддержки):
В ботах периодически бывают так называемые FAQ (разделы с часто задаваемыми вопросами). Был опыт, когда я в админ панели прописывал функционал, позволяющий захватить вопрос и ответ на него (FSM), далее это всё записывалось в базу данных под определённым айдишником.
После, когда пользователь заходит в раздел FAQ, бот отправляет запрос в базу данных и при помощи генератора инлайн кнопок (InlineKeyboardBuilder по типу такого же как для текстовых кнопок) происходит генерация клавиатуры с вопросом и ответом.
Далее боту достаточно всего одного хендлера для того чтобы массово обрабатывать сразу все ответы на любые вопросы. Ниже пример реализации:
@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, так как технология, по моему мнению, шикарная. Если сейчас пока не понятно что к чему – не переживайте. Дочитав эту статью, вы точно разберётесь с темой CallBack.
Приступаем к коду.
Код я буду писать в том же проекте, что и писал в прошлых статьях (если хотите такой же шаблон как у меня – читайте первую статью по теме aiogram – там я подробно расписал свой каркас бота стартового).
Давайте в нашем пакете keyboards создадим новый файл под Inline клавиатуры и дадим ему название inline_kbs.py
:
В него сразу импортируем следующие модули:
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
from aiogram.utils.keyboard import InlineKeyboardBuilder
Импортируемые модули в файле inline_kbs.py
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
InlineKeyboardMarkup
: Этот класс используется для создания разметки инлайн клавиатуры. Разметка определяет, как будут располагаться кнопки на клавиатуре и как они будут взаимодействовать с пользователем.InlineKeyboardButton
: Этот класс представляет собой отдельную кнопку на инлайн клавиатуре. С помощью него мы можем задавать текст кнопки и действие, которое произойдет при нажатии на неё, например, отправку CallBack данных.WebAppInfo
: Этот класс используется для создания кнопок, которые открывают веб-приложения внутри Telegram (тема заслуживает отдельного большого обсуждения, так что далее просто покажу, что оно существует). С его помощью можно определить URL веб-приложения, которое будет открыто при нажатии на кнопку. Это полезно для интеграции внешних веб-сервисов и приложений с ботом.
from aiogram.utils.keyboard import InlineKeyboardBuilder
InlineKeyboardBuilder
: Это удобный инструмент для построения инлайн клавиатур. С его помощью можно легко и быстро создавать клавиатуры, добавляя кнопки и определяя их расположение. Этот класс помогает упрощать процесс создания сложных разметок клавиатур, делая код более читаемым и удобным для поддержки. Работает похожим образом сReplyKeyboardBuilder
, но со своими особенностями, о которых мы сегодня поговорим.
Инлайн клавиатура со ссылками
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()
:Функция
ease_link_kb
предназначена для создания и возвращения инлайн клавиатуры с кнопками, которые ведут к различным ссылкам различных типов.
Создание списка кнопок
inline_kb_list
:Внутри функции создаётся список
inline_kb_list
, который содержит вложенные списки с объектамиInlineKeyboardButton
. Каждая вложенная структура представляет собой отдельную строку кнопок на инлайн клавиатуре.
Кнопка с ссылкой на мой аккаунт в Хабре:
[InlineKeyboardButton(text="Мой хабр", url='https://habr.com/ru/users/yakvenalex/')]
:Создаётся кнопка с текстом "Мой хабр", которая при нажатии перенаправляет пользователя на страницу Хабра.
Кнопка с ссылкой на мой Telegram аккаунт:
[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)
:Функция возвращает объект
InlineKeyboardMarkup
, который содержит разметку инлайн клавиатуры с указанными кнопками.
Этот пример демонстрирует, как создавать инлайн клавиатуру с различными типами ссылок, включая обычные URL, ссылки на аккаунты в Telegram и веб-приложения. Давайте тестировать.
Для тестов я предлагаю создать новый message handler, который будет вызываться текстом «Давай инлайн!». К нему прикрутим нашу инлайн клавиатуру и поклацаем ее.
@start_router.message(F.text == 'Давай инлайн!')
async def get_inline_btn_link(message: Message):
await message.answer('Вот тебе инлайн клавиатура со ссылками!', reply_markup=ease_link_kb())
Думаю, что к настоящему моменту вы уже понимаете, что тут мы использовали магический фильтр F.text, который будет срабатывать на отправку текста 'Давай инлайн!', а чтоб было ещё интересней давайте мы создадим текстовую кнопку с текстом 'Давай инлайн!', а саму кнопку привяжем к главной клавиатуре (это вы уже умеете делать, если нет, то читайте прошлую статью).
Давайте теперь изучать каждую кнопку-ссылку:
После клика на обычную кнопку-ссылку появляется окно которое спрашивает хотим ли мы перейти по ссылке.
После клика на ссылку с моим профилем Telegram происходит переход без окна в мой профиль. До недавнего времени только ссылки формата «tg://resolve?domain=yakvenalexx» позволяли переходить в профиль без окна, но, при написании этого текста обнаружил что и при формате ссылки «https://t.me/yakvenalexx» окно не появлялось.
Теперь к интересному моменту ВЕБ-ПРИЛОЖЕНИЕ:
После клика по инлайн кнопке с «квадратиком» телеграмм отправит нам такое сообщение:
А после откроется то ВЕБ-ПРИЛОЖЕНИЕ, которое я создал. С ПК версии, возможно не так наглядно как с телефона. Сейчас продемонстрирую.
Обратите внимание на интерактивность. Веб-приложение становится, как бы, частью телеграмм бота, тем самым открывая неограниченные возможности. Вот так, например, я прописывал специальную анкету в одном из своих ботов:
Как я говорил выше – все это тема отдельной статьи. Может как-то обсудим.
Итак, к промежуточным выводам. Мы разобрали все варианты инлайн-клавиатур ссылок, а это значит, что можем переходить к более интересной части – CallBack Data!
Начнем с простой клавиатуры. Пускай в ней будет 2 инлайн-кнопки. Одна кнопка должна переводить пользователя на стартовый экран, а вторая запускает некое действие, например, выводит информацию о случайном пользователе (первое, что пришло в голову).
Для этого нам нужно подготовиться.
Сначала напишем функцию, которая будет генерировать информацию о случайном пользователе. Для этого воспользуемся интересной библиотекой Faker (возьмите её на заметку, часто пригождается).
Устанавливаем библиотеку:
pip install faker
Пишем функцию, которая будет генерировать случайного пользователя (как раз наш пакет с утилитами тут будет кстати).
Пишем код:
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 для удобства аннотаций.
from utils.utils import get_random_person
from aiogram.types import CallbackQuery
Теперь напишем сам хендлер. Я его пропишу полностью, а дальше дам объяснения.
@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, в котором мы ведем разработку бота (у меня это Pycharm).
Обратите внимание, что я закомментировал одну строку. Это сделано намеренно, и далее вы поймете зачем.
После мы сгенерировали нашего пользователя через функцию, которую писали ранее, а затем приступили к простому форматированию текста. Если вы пользуетесь моей структурой бота, то у вас тоже при инициализации указывалось:
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 и приступим к тестированию.
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'
мы уже написали).
Давайте эту клавиатуру привяжем вместо клавиатуры со ссылками. Отлично. Запускаем бота и смотрим, что у нас получилось:
Мы видим, что информация о пользователе сгенерирована, но кнопка не перестает мигать (знаю, что на скрине это плохо видно и понятно, но, поверьте, она мигает, а на смартфоне будут часики возле кнопки). Дело в том, что инлайн-клавиатуры в Telegram устроены таким образом, что они всегда ждут ответа от хендлера, что тот выполнен. Ждут порядка 30 секунд, после чего успокаиваются.
Давайте мы дадим ответ серверам Telegram, что все ок и по плану:
await call.answer('Генерирую случайного пользователя', show_alert=False)
show_alert=False
идет по умолчанию, но я оставлю это в коде, чтобы вас не путать. Перезапускаем бота и смотрим:
Мы видим, что появилась белая надпись на черной плашке (висит секунды 2-3) и, при этом, у нас кнопка не мигает больше. Отлично.
Если вам нечего сообщать пользователю, можете просто указывать await call.answer()
, тогда кнопка будет мгновенно тухнуть. Давайте сменим теперь флаг show_alert
на True
и посмотрим, что у нас получится:
Теперь у нас появляется окно alert, и для продолжения нужно будет нажать на «Ок». В некоторых случаях это бывает удобно.
Теперь, для закрепления материала, давайте пропишем хендлер для обработки callback_data='back_home'
. Выполните самостоятельно, уверен, что у вас все получится.
Генератор инлайн-клавиатур - InlineKeyboardBuilder
Сейчас мы не просто разберем генератор инлайн-клавиатур (это, на самом деле, не трудно), но и попробуем написать свой первый более-менее серьезный обработчик. Смысл всей затеи будет таким:
Мы соберем некий массив данных (питоновский словарь наподобие JSON). Пусть это будут вопросы с ответами. Словарь будет иметь ключ в виде целого числа и принадлежащий ему массив данных в виде вопроса и ответа.
Мы напишем функцию, которая будет генерировать инлайн-клавиатуры такого вида:
text
= «Вопрос»callback_data
= f-строка, содержащая приставкуqst_
и ID вопроса
Универсальный хендлер, который будет давать ответ на каждый вопрос.
Я знаю, что уже давал выше пример такой функции из «боевого» бота. Тут же мы пропишем её упрощенную версию.
Начнем с массива вопросов и ответов. Напишем таких вопросов 10 штук. Вот пример:
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
Теперь напишем функцию, которая будет принимать словарь вопросов и возвращать инлайн-клавиатуру:
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» (для закрепления данных из прошлых моих статей можете закрепить эту команду в командном меню).
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())
У меня получился такой результат. Пишем обработчик.
@start_router.message(Command('faq'))
async def cmd_start_2(message: Message):
await message.answer('Сообщение с инлайн клавиатурой с вопросами', reply_markup=create_qst_inline_kb(questions))
Вот простой обработчик. Вопросы я импортировал из файла create_bot:
from create_bot import questions
Запускаем и смотрим что у нас получилось:
Видим, что вопросы благополучно подгрузились. Теперь нам остается написать универсальный обработчик, который будет отвечать на выбранный вопрос. Давайте к каждому ответу прикрутим инлайн клавиатуру с вопросами.
@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))
Не волнуйтесь, сейчас со всем разберемся. Для начала выполним импорты:
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 и трансформируем строку в айдишник вопроса, тем самым, давая боту понять ответ на какой вопрос мы хотим получить. Технически call.data – это самая обыкновенная строка, а значит с ней можно делать все что с обычными строками. Понимаете к чему я?
То есть в одной call_data вы можете передать много информации, например айди пользователя, сумма оплаты и идентификатор товара, который пользователь покупает в вашем боте (это реальный пример одного из моих ботов).
Выглядит так: «order_112344_232_1245»
Далее специальный хендлер срабатывает на F.data.startswith(order_')), а далее из строки забирает нужные ему данные. Представляете какие это возможности открывает?
Ну и пока вы под впечатлением (надеюсь) мы продолжим разбирать наш код.
Далее мы достаем нужный нам вопрос, через его айди, а после просто формируем сообщение, которое бот отправит пользователю через f-строку, забирая нужные нам данные.
Ну а дальше вы уже знаете. Сделали имитацию набора текста на 2 секунды и затем отправили сообщение с клавиатурой. Смотрим на примере:
Как вы видите бот начал имитировать набор текста. А вот и ответ:
Выводы
Друзья, сегодня мы разобрали очень важную тему, которая откроет вам двери к созданию телеграмм-ботов любой сложности. Если какие-то моменты остались неясными, не стесняйтесь задавать вопросы в комментариях. Честно говоря, когда я только начинал, тоже не сразу все понял, но со временем ощутил все преимущества работы с CallBack Data.
В остальном все как обычно. Если этот текст был полезен и, благодаря нему вы узнали что-то новое, не забудьте об это сообщить в комментариях, своим лайком или подпиской. Написание подобного рода туториолов требует много времени и усилий, а без вашей поддержки все это потеряет смысл.
До скорого!