Разработка · 6 мин чтения

F.data aiogram 3: фильтры колбэков и разбор данных кнопки

На проектах с Telegram-ботами разбор callback data - то место, где новички обычно упираются в стену: кнопки шлют обратно строки, а не готовые объекты, и без нормального фильтра хендлер обрастает вложенными if на пять экранов. Фильтр F.data aiogram 3 - магический фильтр (magic filter), который решает эту задачу в одну строку декоратора: сравнение, маска, вхождение подстроки - всё это можно описать прямо в аргументах хендлера, не трогая тело функции. Ниже - как я строю такие фильтры на практике, с примерами из ботов для записи на консультацию, уведомлений о статусе доставки СДЭК и интеграций с CRM.

Зачем нужен фильтр F.data в aiogram 3

Когда пользователь нажимает inline-кнопку, Telegram присылает боту объект CallbackQuery, а внутри него - поле data, обычная строка длиной до 64 байт. Никакой типизации, никакой структуры по умолчанию - что записали в callback_data при создании кнопки, то и придёт назад. В aiogram 2 это разбирали через callback_data.split(“:”) и кучу ручных проверок. В aiogram 3 для этого есть объект F - обёртка над magic filter, которая позволяет описывать условие декларативно, не открывая тело хендлера.

Самый простой случай - точное совпадение:

from aiogram import F, Router
from aiogram.types import CallbackQuery

router = Router()

@router.callback_query(F.data == "confirm_order")
async def confirm_order(callback: CallbackQuery):
    await callback.answer("Заказ подтверждён")
    await callback.message.edit_text("Спасибо, заказ в обработке")

Важный момент, о который спотыкаются почти все: callback.answer() нужно вызывать всегда, даже если не собираетесь показывать всплывающее уведомление. Без него у пользователя кнопка «крутится» до таймаута - Telegram ждёт подтверждения от бота, что колбэк обработан.

Разбор callback_data: от точного совпадения до масок

Живой бот редко ограничивается одной кнопкой с одним значением. На практике у меня почти в каждом проекте есть пагинация, категории, фильтры - и на каждый вариант заводить отдельный F.data == “…” неудобно.

Точное совпадение и списки значений

Если вариантов немного и они известны заранее, удобно сравнивать с множеством через in_:

@router.callback_query(F.data.in_({"yes", "no"}))
async def answer_yes_no(callback: CallbackQuery):
    if callback.data == "yes":
        await callback.message.answer("Записал вас на приём")
    else:
        await callback.message.answer("Хорошо, отменил запись")

Проверка вхождения и префиксов

Для пагинации и категорий я почти всегда использую префикс с разделителем и startswith:

@router.callback_query(F.data.startswith("page_"))
async def paginate(callback: CallbackQuery):
    page = int(callback.data.split("_")[1])
    await show_page(callback, page)

Есть ещё F.data.contains(“…”) и F.data.endswith(“…”) - они реже нужны, но выручают, когда в одной строке смешаны идентификатор и статус, например order_42_paid.

Слабое место этого подхода одно и то же везде: строковый разбор ломается тихо. Опечатался в разделителе, добавил в id заказа символ подчёркивания - и split даёт не тот индекс, а исключение вылетает не в фильтре, а глубоко в хендлере, когда его уже сложно связать с причиной.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

CallbackData фабрика: структурированный разбор данных кнопки

Когда в кнопке нужно передать больше одного значения - id заказа, действие, страницу - я перехожу на CallbackData из aiogram.filters.callback_data. Это класс на pydantic, который сам сериализует поля в строку с разделителем и разбирает её обратно, плюс следит за лимитом в 64 байта.

from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup

class OrderCallback(CallbackData, prefix="order"):
    action: str
    order_id: int

def order_keyboard(order_id: int) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup(inline_keyboard=[[
        InlineKeyboardButton(
            text="Подтвердить",
            callback_data=OrderCallback(action="confirm", order_id=order_id).pack()
        ),
        InlineKeyboardButton(
            text="Отменить",
            callback_data=OrderCallback(action="cancel", order_id=order_id).pack()
        ),
    ]])

Фильтр на такую кнопку строится через .filter(), а в аргументы хендлера aiogram сам прокинет уже распакованный объект:

@router.callback_query(OrderCallback.filter(F.action == "confirm"))
async def confirm_handler(callback: CallbackQuery, callback_data: OrderCallback):
    order_id = callback_data.order_id
    await mark_order_paid(order_id)
    await callback.answer("Оплата подтверждена")

На боте для приёма заявок, который я связывал с CRM и приёмом оплат через T‑Bank, именно эта конструкция сняла добрую половину багов: order_id всегда int, action всегда одно из объявленных значений, а если менеджер случайно передаст в кнопку строку длиннее лимита, pydantic упадёт на этапе создания клавиатуры, а не через неделю на проде при нажатии кнопки живым клиентом.

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

Комбинирование F.data с другими фильтрами

Один F.data редко покрывает всю логику хендлера. На практике его комбинируют со StateFilter (текущий шаг FSM) и с проверкой отправителя:

@router.callback_query(
    OrderCallback.filter(F.action == "cancel"),
    StateFilter(OrderStates.waiting_confirm),
)
async def cancel_in_progress(callback: CallbackQuery, callback_data: OrderCallback, state: FSMContext):
    await state.clear()
    await callback.message.edit_text("Заявка отменена")

Перечисленные через запятую фильтры в aiogram работают как логическое И - сработают все сразу. Через операторы & и | можно строить более сложные условия прямо внутри F.data, например F.data.startswith(“admin_”) & ~F.from_user.id.in_(admin_ids), чтобы отсечь чужие нажатия на административные кнопки в общем чате.

Вот сводка, какой вариант когда выбираю:

Способ Когда использую Плюсы Минусы
F.data == “…” Одна фиксированная кнопка (подтвердить, отмена, назад) Максимально просто Не масштабируется на параметры
F.data.startswith / contains Пагинация, простые категории Не нужен отдельный класс Хрупкий парсинг строки вручную
F.data.in_({…}) Небольшой фиксированный набор значений Читаемо, без опечаток в проверках Неудобно при десятках вариантов
CallbackData фабрика Несколько параметров в кнопке (action + id + …) Валидация, автоматическая упаковка/распаковка Чуть больше кода на старте

Частые ошибки при работе с фильтрами колбэков в aiogram 3

За несколько лет разработки ботов на aiogram эти грабли повторяются почти у каждого второго заказчика, который приходит с готовым, но глючным ботом на доработку.

Первое - лимит в 64 байта на callback_data. Если пытаетесь запихнуть в кнопку JSON с несколькими полями и длинными названиями через обычную f‑строку, легко вылезти за лимит на реальных данных, хотя в тестах с короткими id всё работало. CallbackData фабрика с короткими именами полей и prefix решает это надёжнее, чем ручная сериализация.

Второе - порядок регистрации хендлеров. aiogram проверяет фильтры сверху вниз и выполняет первый подошедший хендлер. Если сначала стоит общий F.data.startswith(“order_”), а ниже - более узкий F.data == “order_cancel”, второй хендлер никогда не сработает, потому что первый уже перехватил колбэк. На одном боте для записи клиентов я потратил час, разбираясь, почему кнопка отмены не работает - а причина была именно в порядке роутеров.

Третье - отсутствие проверки автора нажатия. Если сообщение с кнопками видно нескольким пользователям (например, в группе или пересланное сообщение), любой человек может нажать чужую inline-кнопку. Для действий вроде подтверждения оплаты или отмены записи я всегда добавляю сверку callback.from_user.id с id, для которого создавалась клавиатура - либо сохраняю его в CallbackData, либо в FSM-контексте.

Четвёртое - путаница между callback.data и callback.message.text. Иногда в хендлере по ошибке лезут в текст сообщения вместо данных кнопки, особенно когда переносят логику из обработчика обычных сообщений. Это ловится сразу при первом тесте, но на код-ревью встречается регулярно.

Автоматизация в мессенджере

Telegram-бот / Mini App

от 30 000 ₽

Подробнее →

Частые вопросы

В чём разница между F.data и CallbackData фабрикой в aiogram 3?

F.data - это прямая работа со строкой callback_data: сравнение, startswith, contains и подобные операции. CallbackData фабрика - надстройка на pydantic, которая сама упаковывает несколько полей в строку по заданному префиксу и разбирает её обратно в типизированный объект. Для одной фиксированной кнопки хватает F.data, для кнопок с параметрами (id, действие, страница) удобнее фабрика.

Можно ли передать в callback_data сложный объект вроде JSON?

Технически строку можно набить произвольным содержимым, но лимит в 64 байта делает JSON рискованным - он быстро съедает лимит на скобках, кавычках и именах полей. Для нескольких параметров лучше CallbackData фабрика с короткими именами полей, а для действительно объёмных данных - хранить их на своей стороне (в базе или FSM-контексте) и передавать в кнопке только короткий идентификатор записи.

Как ограничить обработку колбэка только автором исходного сообщения?

Сохраняю id пользователя либо прямо в CallbackData (отдельным полем), либо в состоянии FSM при показе клавиатуры, а в хендлере сверяю его с callback.from_user.id перед выполнением действия. Если id не совпадает, отвечаю через callback.answer с коротким текстом вроде “Эта кнопка не для вас” и show_alert=True, ничего не выполняя.

Что делать, если callback_data превышает разрешённые 64 байта?

aiogram и сама CallbackData фабрика поднимут ошибку при попытке упаковать слишком длинные данные - это надёжнее, чем ловить обрезанную строку в проде. Решение обычно одно: сократить имена полей и хранимые значения (например, использовать числовой id вместо строкового slug), а всё, что не помещается, вынести в базу и передавать в кнопке только ссылку на запись.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно