Callback-события
Callback позволяет боту обработать нажатие на inline-кнопку без отправки пользователем отдельного текстового сообщения.
Общий поток выглядит так:
CallbackButton
│
▼
пользователь нажимает кнопку
│
▼
MAX отправляет message_callback
│
▼
router.callback_query
│
▼
CallbackQuery
│
▼
callback.answer(...)
В aiomax2 для обработки таких событий используется observer:
Он является удобным alias для MAX-события:
Быстрый пример
Создадим кнопку:
from aiomax2 import F, Router
from aiomax2.types import CallbackQuery, Message
from aiomax2.utils.keyboard import InlineKeyboardBuilder
router = Router()
@router.message(F.text == "buttons")
async def buttons(message: Message) -> None:
builder = InlineKeyboardBuilder()
builder.button(
text="Подтвердить",
callback_data="confirm",
)
await message.answer(
"Выберите действие",
reply_markup=builder.as_markup(),
)
После нажатия кнопки MAX отправит событие message_callback.
Обработать его можно так:
@router.callback_query(F.payload == "confirm")
async def confirm(callback: CallbackQuery) -> None:
await callback.answer(
notification="Готово",
)
callback_data="confirm" в builder является удобным alias aiomax2.
На уровне MAX API значение передаётся как:
и приходит в:
Подробнее о создании кнопок и клавиатур: Inline-клавиатуры.
CallbackQuery
Handler router.callback_query получает объект CallbackQuery.
from aiomax2.types import CallbackQuery
@router.callback_query()
async def callback_handler(callback: CallbackQuery) -> None:
print(callback.callback_id)
print(callback.payload)
print(callback.timestamp)
print(callback.user)
print(callback.message)
print(callback.user_locale)
Основные поля:
| Поле | Назначение |
|---|---|
callback_id |
идентификатор callback для ответа через POST /answers |
payload |
значение, указанное в callback-кнопке |
timestamp |
время callback-события |
user |
пользователь, нажавший кнопку |
message |
исходное сообщение, если оно доступно |
user_locale |
locale пользователя, если MAX передал его |
CallbackQuery создаётся aiomax2 из MAX-события message_callback.
payload
Основной способ определить, какая кнопка была нажата — поле payload.
Например, клавиатура:
builder = InlineKeyboardBuilder()
builder.button(
text="Да",
callback_data="confirm",
)
builder.button(
text="Нет",
callback_data="cancel",
)
builder.adjust(2)
может обрабатываться двумя handlers:
@router.callback_query(F.payload == "confirm")
async def confirm(callback: CallbackQuery) -> None:
await callback.answer(notification="Подтверждено")
@router.callback_query(F.payload == "cancel")
async def cancel(callback: CallbackQuery) -> None:
await callback.answer(notification="Отменено")
Или одним:
@router.callback_query(F.payload.in_({"confirm", "cancel"}))
async def action(callback: CallbackQuery) -> None:
if callback.payload == "confirm":
await callback.answer(notification="Подтверждено")
return
await callback.answer(notification="Отменено")
Подробнее о F:
Magic filter.
callback_id
Не следует путать:
и:
payload задаётся разработчиком при создании кнопки:
и используется для маршрутизации:
callback_id создаётся MAX и идентифицирует конкретное callback-событие.
Именно callback_id используется при отправке ответа в:
При использовании:
aiomax2 передаёт callback.callback_id автоматически.
Пользователь
Пользователь, нажавший кнопку, доступен через:
Например:
@router.callback_query(F.payload == "profile")
async def profile(callback: CallbackQuery) -> None:
user_id = callback.user.user_id
await callback.answer(
notification=f"Ваш ID: {user_id}",
)
Фильтровать callback можно непосредственно по пользователю:
ADMIN_ID = 123456
@router.callback_query(
F.payload == "admin",
F.user.user_id == ADMIN_ID,
)
async def admin(callback: CallbackQuery) -> None:
await callback.answer(notification="Доступ разрешён")
Несколько filters одного handler работают как логическое AND.
Исходное сообщение
Если MAX передал исходное сообщение, оно доступно через:
Например:
@router.callback_query()
async def callback_handler(callback: CallbackQuery) -> None:
if callback.message is not None:
print(callback.message.message_id)
print(callback.message.text)
print(callback.message.chat_id)
Поле:
имеет тип:
Поэтому нельзя считать, что исходное сообщение присутствует всегда.
Например, оно может отсутствовать, если MAX прислал callback без доступного объекта сообщения.
Правильно:
Не следует без проверки писать:
Ответ на callback
Основной shortcut:
Он вызывает MAX endpoint:
и возвращает:
Простейший вариант:
@router.callback_query(F.payload == "confirm")
async def confirm(callback: CallbackQuery) -> None:
success = await callback.answer(
notification="Готово",
)
print(success)
Уведомление
Через notification можно передать текст одноразового уведомления:
На wire aiomax2 отправит:
без искусственного пустого message.
Это соответствует текущей модели MAX API.
Note
Фактическое отображение notification-only зависит от поведения клиента MAX.
Во время live smoke-test MAX API успешно принимал такой ответ и возвращал
success=true, но отдельное notification-only не отображалось в
протестированных Web, Android и iOS клиентах.
При одновременном изменении сообщения callback answer отображался корректно.
aiomax2 не подменяет это поведение и не добавляет фиктивное сообщение автоматически.
Изменение сообщения через callback
Ответ на callback может одновременно изменить сообщение.
Для простого случая можно передать новый текст:
@router.callback_query(F.payload == "confirm")
async def confirm(callback: CallbackQuery) -> None:
await callback.answer(
text="Статус: подтверждено",
)
text — convenience-параметр aiomax2.
Библиотека автоматически создаст MAX NewMessageBody.
Можно одновременно передать notification:
То есть один callback answer может:
Изменение клавиатуры
Через callback answer можно передать новую inline-клавиатуру:
from aiomax2.utils.keyboard import InlineKeyboardBuilder
@router.callback_query(F.payload == "next")
async def next_page(callback: CallbackQuery) -> None:
builder = InlineKeyboardBuilder()
builder.button(
text="Назад",
callback_data="back",
)
await callback.answer(
text="Страница 2",
reply_markup=builder.as_markup(),
)
reply_markup является удобным Python API.
На уровне MAX клавиатура остаётся attachment типа:
Подробнее: Inline-клавиатуры.
Attachments
При изменении сообщения через callback answer также можно передать attachments:
attachments использует те же AttachmentRequest, что и:
Нельзя одновременно передавать inline keyboard через:
и:
если в attachments уже присутствует inline_keyboard.
aiomax2 в таком случае выбросит ValidationError, чтобы не создать две
клавиатуры неявно.
Форматирование текста
При изменении сообщения можно указать формат:
from aiomax2 import TextFormat
await callback.answer(
text="<b>Готово</b>",
format=TextFormat.HTML,
)
Или Markdown:
Подробнее: Форматирование сообщений.
Отключение preview ссылок
При изменении текста можно передать:
Этот параметр передаётся MAX при обработке callback answer.
NewMessageBody
Вместо convenience-параметров можно сформировать объект сообщения вручную:
from aiomax2.types import NewMessageBody
body = NewMessageBody(
text="Статус: подтверждено",
)
await callback.answer(
message=body,
)
Это low-level вариант.
Обычно проще использовать:
message и convenience-параметры
Нельзя одновременно передавать:
и поля, из которых aiomax2 сам создаёт сообщение:
Например, неправильно:
aiomax2 выбросит ValidationError.
Используйте один из двух вариантов.
Высокоуровневый:
или low-level:
Диагностический ответ MAX
Обычный:
возвращает только:
Этого достаточно для большинства handlers.
Если требуется получить диагностическое сообщение MAX при:
можно использовать:
Например:
result = await bot.answer_callback_result(
callback.callback_id,
notification="Готово",
)
if not result.success:
print(result.message)
Метод возвращает:
вместо одного bool.
Для обычного приложения предпочтительнее:
а answer_callback_result() полезен для диагностики и низкоуровневой
обработки ошибок API.
Ответ через Bot
Shortcut:
в конечном итоге использует:
Низкоуровневый вызов также доступен напрямую:
Но внутри callback handler обычно лучше использовать:
потому что CallbackQuery уже содержит необходимые данные события и
автоматически передаёт идентификатор callback.
Target rate limit
Callback shortcut передаёт в Bot target исходного сообщения.
Если доступен:
он используется как target.
Если исходного сообщения нет, aiomax2 использует:
Это позволяет callback operations участвовать в общей клиентской per-target rate limiting политике aiomax2 вместе с операциями над сообщениями.
Для обычного пользовательского кода дополнительная настройка не требуется.
Пустой answer()
Технически текущая модель MAX API не требует обязательного наличия
notification или message, поэтому aiomax2 локально не запрещает:
Однако такой запрос не содержит полезного действия.
Для реального callback обычно следует передать хотя бы:
или изменение сообщения:
Low-level callback-кнопка
Builder является рекомендуемым способом создания клавиатур:
Но low-level MAX-модель также доступна напрямую:
from aiomax2.types import (
CallbackButton,
InlineKeyboardAttachmentRequest,
Keyboard,
)
keyboard = InlineKeyboardAttachmentRequest(
payload=Keyboard(
buttons=[
[
CallbackButton(
text="Подтвердить",
payload="confirm",
)
]
]
)
)
await message.answer(
"Выберите действие",
attachments=[keyboard],
)
Здесь используется настоящее MAX-поле:
Builder API:
является только удобным alias.
Ограничение callback payload
Текущая модель aiomax2 ограничивает CallbackButton.payload максимум
1024 символами в соответствии с актуальным контрактом MAX.
Например:
При проектировании payload лучше хранить только данные, необходимые для идентификации действия.
Например:
а не сериализовать в кнопку большие объекты состояния приложения.
Структурированный payload
Для небольших приложений достаточно простых строк:
Для более сложной маршрутизации удобно использовать собственный формат:
и фильтровать его через magic filter:
@router.callback_query(F.payload.regexp(r"^order:confirm:(\d+)$").as_("match"))
async def confirm_order(
callback: CallbackQuery,
match: re.Match[str],
) -> None:
order_id = int(match.group(1))
await callback.answer(
notification=f"Заказ {order_id} подтверждён",
)
Для этого примера потребуется:
Подробнее о regexp() и as_():
Magic filter.
Callback без исходного сообщения
aiomax2 поддерживает callback, у которого:
Например:
@router.callback_query()
async def callback_handler(callback: CallbackQuery) -> None:
if callback.message is None:
await callback.answer(
notification="Исходное сообщение недоступно",
)
return
await callback.answer(
text="Сообщение обновлено",
)
При отсутствии сообщения пользователь для внутреннего context и target определяется через:
FSM и callback
Callback handlers работают с тем же FSM context, что и остальные handlers.
Например:
from aiomax2.fsm import FSMContext
@router.callback_query(F.payload == "confirm")
async def confirm(
callback: CallbackQuery,
state: FSMContext,
) -> None:
await state.update_data(confirmed=True)
await callback.answer(
notification="Подтверждено",
)
Если исходное сообщение отсутствует, aiomax2 может определить идентификаторы FSM по пользователю callback.
Подробнее: FSM.
Регистрация без декоратора
Callback handler можно зарегистрировать через .register():
async def confirm(callback: CallbackQuery) -> None:
await callback.answer(notification="Готово")
router.callback_query.register(
confirm,
F.payload == "confirm",
)
Это эквивалентно:
@router.callback_query(F.payload == "confirm")
async def confirm(callback: CallbackQuery) -> None:
await callback.answer(notification="Готово")
Alias observer
В Router:
является alias для:
Оба работают с MAX update type:
Поэтому:
и:
регистрируются в одном observer.
Для прикладного кода обычно удобнее:
а message_callback полезно знать при сопоставлении кода с названиями
событий MAX API.
Краткая справка
| Задача | Пример |
|---|---|
| Обработать callback | @router.callback_query() |
| Проверить payload | F.payload == "confirm" |
| Получить payload | callback.payload |
| Получить ID callback | callback.callback_id |
| Получить пользователя | callback.user |
| Получить исходное сообщение | callback.message |
| Показать notification | callback.answer(notification="Готово") |
| Изменить текст | callback.answer(text="Готово") |
| Изменить клавиатуру | callback.answer(reply_markup=...) |
| Получить диагностический результат | bot.answer_callback_result(...) |
Для создания inline-клавиатур и всех типов кнопок MAX используйте: Inline-клавиатуры.
Для фильтрации callback payload: Magic filter.