урок 3 из 10

Обработчики и фильтры

Ловить команды и их аргументы; отбирать сообщения волшебным фильтром F по тексту и виду, сочетая условия через &, | и ~; различать человека (from_user) и чат (chat), понимать режим приватности в группах; ставить частные обработчики выше общих, держать страховку в конце и проверять доступ по номеру.

10 мин чтения · 4 главы ·5 заданий ·домашняя работа

Команды и аргументы

Во втором уроке у нас было два обработчика: на /start и на всё остальное. Настоящий бот различает больше: команды, слова, фото, голосовые. Условие, по которому обработчик выбирает сообщения, называется фильтром. Самые простые фильтры — команды.

from aiogram.filters import Command, CommandStart, CommandObject

@dp.message(CommandStart())
async def start(message: Message):
    await message.answer("Привет! Команды: /help, /price")

@dp.message(Command("help"))
async def help_(message: Message):
    await message.answer("Я показываю цены. Напишите /price и номер товара.")

Command("help") срабатывает на /help. Команда — это сообщение, которое начинается с косой черты; Телеграм подсвечивает её синим, и по ней можно нажать. Если у бота задан список команд у BotFather, человек видит подсказки, как только наберёт «/».

У команды бывают аргументы — всё, что написано после неё через пробел. «/price 42» — команда price с аргументом «42». Чтобы их получить, в обработчик добавляют параметр command:

@dp.message(Command("price"))
async def price(message: Message, command: CommandObject):
    if not command.args:
        await message.answer("Напишите номер товара: /price 42")
        return
    await message.answer(f"Ищу товар {command.args}…")

В command.args лежит строка после команды или ничего, если человек написал просто /price. Проверка на пустоту обязательна: люди нажимают команду в подсказке, и аргументов у неё нет. Ответ «напишите так-то» лучше молчания и лучше ошибки.

Имя функции-обработчика может быть любым, но лучше говорящим: start, help_, price. Подчёркивание в help_ — потому что help в Python уже занято встроенной функцией.

Фильтр F

Для всего остального в aiogram есть волшебный фильтр F. Он читается почти как фраза: «у сообщения такое-то поле такое-то».

from aiogram import F

@dp.message(F.text == "Привет")
async def hello(message: Message):
    await message.answer("И вам привет!")

@dp.message(F.photo)
async def photo(message: Message):
    await message.answer("Красивое фото!")

@dp.message(F.voice)
async def voice(message: Message):
    await message.answer("Голосовые я пока не слушаю, напишите текстом.")

F.text == "Привет" — текст ровно «Привет», с большой буквы и без точки. «привет» или «Привет!» не подойдут: сравнение точное. Если нужно мягче, текст приводят к одному виду: F.text.lower() == "привет" пропустит и «ПРИВЕТ», и «привет». Есть и проверки на начало и вхождение: F.text.startswith("заказ"), F.text.contains("доставка").

F.photo, F.voice, F.document, F.sticker — фильтры по виду сообщения: пропускают, если у сообщения есть такое поле. Так бот по-разному реагирует на фото, голосовое и файл.

Фильтры можно сочетать: & — «и то, и другое», | — «или то, или другое», ~ — «не». Например, F.photo | F.document пропустит и фото, и файл. Скобки обязательны, когда условий несколько: (F.text == "да") | (F.text == "Да").

Можно передать и несколько фильтров через запятую — они должны сработать все сразу, как через «и». Это удобно, когда сочетают команду и условие: только эта команда и только от такого-то человека.

Кто и где написал

В каждом сообщении бот знает, кто написал и где. Это два разных вопроса, и путать их — частая ошибка.

Личный чатchat.id = 58201937from_user.id = 58201937совпадаютГруппаchat.id = -1001234567890from_user.id — у каждого свойне совпадаютотвечают в чат, запоминают человека
В личке номер чата совпадает с номером человека. В группе чат один, людей много, и номер группы отрицательный.

message.from_user — человек: его номер id, имя first_name и, если он его завёл, адрес username. Номер у человека постоянный и уникальный — именно по нему бот запоминает, кто есть кто. Имя можно поменять, а адреса у многих нет вовсе, поэтому по имени людей не различают.

message.chat — чат, где написано сообщение: его номер id и вид type — private (личка), group или supergroup (группы), channel. В личке номер чата и номер человека совпадают. В группе чат один на всех, а люди разные, и номер группы — отрицательное число.

@dp.message(Command("me"))
async def me(message: Message):
    user = message.from_user
    await message.answer(
        f"Вы: {user.first_name}, номер {user.id}\n"
        f"Чат: {message.chat.type}, номер {message.chat.id}"
    )

Отсюда правило: отвечают в чат, запоминают человека. Если бот ведёт запись к мастеру, заявку привязывают к from_user.id — тогда человек найдёт её из любого чата. А ответ отправляют в chat.id — туда, где его ждут.

В группах есть ещё одна особенность — режим приватности. По умолчанию бот в группе видит не всё: только команды, ответы на свои сообщения и упоминания по имени. Это защищает переписку участников. Отключить режим можно у BotFather, но делать это стоит, только если бот правда должен читать всё — например, модерирует чат.

Порядок обработчиков

Когда обработчиков становится десяток, главным становится порядок. Распорядитель проверяет их сверху вниз и отдаёт сообщение первому, чьи фильтры сказали «да». Остальные даже не узнают о нём.

сообщение«фото»/start?нетF.text?нетF.photo?дапервый подходящий забирает сообщение
Сообщение проходит обработчики сверху вниз. Первый, чьи фильтры сказали «да», забирает его; дальше проверка не идёт.

Правило: частное — выше общего. Обработчик «текст ровно "Привет"» ставят выше обработчика «любой текст», а «любое сообщение» — в самый низ. Если перепутать, частный обработчик никогда не сработает, и никакой ошибки об этом не будет — просто бот «не понимает» слово, хотя код для него написан.

Что происходит с сообщением, которое не подошло ни под один фильтр? Оно молча пропадает. Бот не ответит, и человек решит, что он сломался. Поэтому у бота почти всегда есть последний обработчик-страховка без фильтра, который отвечает: «не понял, вот что я умею».

Фильтром удобно и ограничивать доступ. Команды для владельца — выгрузка заявок, рассылка — закрывают проверкой номера:

ADMIN_ID = 58201937

@dp.message(Command("stats"), F.from_user.id == ADMIN_ID)
async def stats(message: Message):
    await message.answer("Заявок сегодня: 12")

Два фильтра через запятую должны сработать оба: команда /stats и написал владелец. Для остальных этот обработчик не существует — их /stats уйдёт дальше, к страховке. Проверять доступ по имени или адресу нельзя: их можно поменять и выдать себя за другого, а номер — нельзя.

Задания урока

Первые задание решается прямо здесь — без регистрации. Щёлкните по ответу.

  1. 1. Что происходит с сообщением, которое не подошло ни под один обработчик?

Остальные задания урока — в курсе, с сохранением прогресса и разбором ошибок.

  1. Выберите слово.средне
  2. Пропустит ли фильтр F.text == "Привет"?средне
  3. Постоянного пользователя бот надёжнее узнаёт по номеру from_user.id, чем по имени.средне
  4. Расставьте обработчики сверху вниз.средне
Пройти урок с проверкой заданий Курс «Телеграм-боты на Python» открыт и бесплатен, прогресс сохраняется.
Открыть курс