Команды и аргументы
Во втором уроке у нас было два обработчика: на /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 == "Да").
Можно передать и несколько фильтров через запятую — они должны сработать все сразу, как через «и». Это удобно, когда сочетают команду и условие: только эта команда и только от такого-то человека.
Кто и где написал
В каждом сообщении бот знает, кто написал и где. Это два разных вопроса, и путать их — частая ошибка.
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, но делать это стоит, только если бот правда должен читать всё — например, модерирует чат.
Порядок обработчиков
Когда обработчиков становится десяток, главным становится порядок. Распорядитель проверяет их сверху вниз и отдаёт сообщение первому, чьи фильтры сказали «да». Остальные даже не узнают о нём.
Правило: частное — выше общего. Обработчик «текст ровно "Привет"» ставят выше обработчика «любой текст», а «любое сообщение» — в самый низ. Если перепутать, частный обработчик никогда не сработает, и никакой ошибки об этом не будет — просто бот «не понимает» слово, хотя код для него написан.
Что происходит с сообщением, которое не подошло ни под один фильтр? Оно молча пропадает. Бот не ответит, и человек решит, что он сломался. Поэтому у бота почти всегда есть последний обработчик-страховка без фильтра, который отвечает: «не понял, вот что я умею».
Фильтром удобно и ограничивать доступ. Команды для владельца — выгрузка заявок, рассылка — закрывают проверкой номера:
ADMIN_ID = 58201937
@dp.message(Command("stats"), F.from_user.id == ADMIN_ID)
async def stats(message: Message):
await message.answer("Заявок сегодня: 12")
Два фильтра через запятую должны сработать оба: команда /stats и написал владелец. Для остальных этот обработчик не существует — их /stats уйдёт дальше, к страховке. Проверять доступ по имени или адресу нельзя: их можно поменять и выдать себя за другого, а номер — нельзя.