Appearance
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))
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Сильно не вдавайтесь сейчас в подробности кода, в будущих статьях я вас научу делать каждую реализацию. Тут просто смысл в демонстрации мощи CallBackData
— буквально пару строк кода способны закрыть огромный блок FAQ (масштабирование, по сути, неограниченное).
Если сейчас пока не понятно что к чему – не переживайте. Дочитав эту статью, вы точно разберётесь с темой CallBack
.
Приступаем к коду.
Код я буду писать в том же проекте, что и писал в прошлых статьях (если хотите такой же шаблон как у меня – читайте первую статью по теме aiogram – там я подробно расписал свой каркас бота стартового).
Давайте в нашем пакете keyboards создадим новый файл под Inline
клавиатуры и дадим ему название inline_kbs.py
:
В него сразу импортируем следующие модули:
python
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
from aiogram.utils.keyboard import InlineKeyboardBuilder
1
2
2
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)
1
2
3
4
5
6
7
2
3
4
5
6
7
Функция
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())
1
2
3
2
3
Думаю, что к настоящему моменту вы уже понимаете, что тут мы использовали магический фильтр F.text
, который будет срабатывать на отправку текста 'Давай инлайн!', а чтоб было ещё интересней давайте создадим текстовую кнопку с текстом 'Давай инлайн!', а саму кнопку привяжем к главной клавиатуре .
Так теперь выглядит главная клавиатура
Так выглядит инлайн клавиатура со ссылками
Давайте теперь изучать каждую кнопку-ссылку:
После клика на обычную кнопку-ссылку появляется окно которое спрашивает хотим ли мы перейти по ссылке.
После клика на ссылку с моим профилем Telegram происходит переход без окна в мой профиль. До недавнего времени только ссылки формата tg://resolve?domain=yakvenalexx
позволяли переходить в профиль без окна, но, при написании этого текста обнаружил что и при формате ссылки https://t.me/yakvenalexx
окно не появлялось.
Теперь к интересному моменту ВЕБ-ПРИЛОЖЕНИЕ:
После клика по инлайн кнопке с «квадратиком» телеграмм отправит нам такое сообщение:
Пример простого приложения
А после откроется то ВЕБ-ПРИЛОЖЕНИЕ, которое я создал. С ПК версии, возможно не так наглядно как с телефона. Сейчас продемонстрирую.
Обратите внимание на интерактивность. Веб-приложение становится, как бы, частью телеграмм бота, тем самым открывая неограниченные возможности. Вот так, например, я прописывал специальную анкету в одном из своих ботов:
Итак, к промежуточным выводам. Мы разобрали все варианты инлайн-клавиатур ссылок, а это значит, что можем переходить к более интересной части – CallBack Data!
Начнем с простой клавиатуры. Пускай в ней будет 2 инлайн-кнопки. Одна кнопка должна переводить пользователя на стартовый экран, а вторая запускает некое действие, например, выводит информацию о случайном пользователе (первое, что пришло в голову).
Для этого нам нужно подготовиться.
Сначала напишем функцию, которая будет генерировать информацию о случайном пользователе. Для этого воспользуемся интересной библиотекой Faker (возьмите её на заметку, часто пригождается).
Устанавливаем библиотеку:
shell
pip install faker
1
Пишем функцию, которая будет генерировать случайного пользователя (как раз наш пакет с утилитами тут будет кстати).
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Импортируем модуль. При инициализации объекта fake
укажем, что нас интересуют русские данные. Далее создадим простой словарь и захватим в него следующие данные: имя, адрес, email, телефон, дата рождения, компания и работа.
Далее мы напишем специальный хендлер, который при получении CallData
get_person
будет возвращать хорошо оформленное сообщение с информацией о пользователе (как раз немного углубим свои знания в теме форматирования текста в aiogram 3
).
Сначала импортируем функцию для генерации случайного пользователя из пакета utils
и CallbackQuery
для удобства аннотаций.
python
from utils.utils import get_random_person
from aiogram.types import CallbackQuery
1
2
2
Теперь напишем сам хендлер. Я его пропишу полностью, а дальше дам объяснения.
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Тут у нас изменился декоратор. Теперь это не @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))
1
Тем самым мы научили бота воспринимать 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)
1
2
3
4
5
6
2
3
4
5
6
Вы можете обратить внимание, что клавиатура не особо отличается от клавиатуры со ссылками. Единственное отличие в том, что вместо url
мы передаем callback_data
, на которые и будет реагировать наш бот (обработчик под callback_data='get_person'
мы уже написали).
Давайте эту клавиатуру привяжем вместо клавиатуры со ссылками. Отлично. Запускаем бота и смотрим, что у нас получилось:
Немного изменил текст в хендлере, раньше бот говори про ссылки
Мы видим, что информация о пользователе сгенерирована, но кнопка не перестает мигать (знаю, что на скрине это плохо видно и понятно, но, поверьте, она мигает, а на смартфоне будут часики возле кнопки). Дело в том, что инлайн-клавиатуры в Telegram устроены таким образом, что они всегда ждут ответа от хендлера, что тот выполнен. Ждут порядка 30 секунд, после чего успокаиваются.
Давайте мы дадим ответ серверам Telegram, что все ок и по плану:
python
await call.answer('Генерирую случайного пользователя', show_alert=False)
1
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 штук. Вот пример:
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': 'Тихий океан'},
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Этот массив я прописал в файле 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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Функция генератор инлайн клавиатуры не сильно отличается от своего аналога в генерации текстовых клавиатур. Она принимает питоновский словарь с вопросами, а после пробегается по каждому генерируя нужные нам кнопки.
Обратите внимание, что в конец этой клавиатуры я добавил 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())
1
2
3
4
5
2
3
4
5
Получился такой результат. Пишем обработчик.
python
@start_router.message(Command('faq'))
async def cmd_start_2(message: Message):
await message.answer('Сообщение с инлайн клавиатурой с вопросами', reply_markup=create_qst_inline_kb(questions))
1
2
3
2
3
Вот простой обработчик. Вопросы я импортировал из файла create_bot
:
python
from create_bot import questions
1
Запускаем и смотрим что у нас получилось:
Видим, что вопросы благополучно подгрузились. Теперь нам остается написать универсальный обработчик, который будет отвечать на выбранный вопрос. Давайте к каждому ответу прикрутим инлайн клавиатуру с вопросами.
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))
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Не волнуйтесь, сейчас со всем разберемся. Для начала выполним импорты:
python
import asyncio
from aiogram.utils.chat_action import ChatActionSender
from create_bot import questions, bot
1
2
3
2
3
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 секунды и затем отправили сообщение с клавиатурой. Смотрим на примере:
Имитация набора текста 2 секунды
Как вы видите бот начал имитировать набор текста. А вот и ответ: