Хэндлеры сообщений и команд
Хэндлер — это функция, которую aiogram вызывает в ответ на нужное сообщение или команду, а роутер и фильтры решают, какому хэндлеру какое сообщение достанется.
Хэндлер — функция-обработчик, которую aiogram вызывает в ответ на определённое сообщение, команду или нажатие кнопки.
Зачем тебе это нужно
Помнишь, в уроке «Первый бот: запуск и /start» наш «Цыплёнок-помощник» уже умел отвечать на одну-единственную команду /start? Это здорово, но представь живого бота-напоминалку, которым ты делишься с друзьями перед контрольной. Один пишет ему /start, другой шлёт /help, третий просто отправляет «привет», а четвёртый кидает домашку текстом, чтобы бот её сохранил. Бот должен на каждое из этих сообщений ответить по-своему — и при этом не запутаться.
Вот к чему мы придём в этом уроке: бот, который различает команды и обычные сообщения и отвечает на каждое осмысленно.
Ты: /start
Бот: Привет! Я Цыплёнок-помощник 🐤 Напиши /help, чтобы узнать, что я умею.
Я: /help
Бот: Я понимаю команды /start и /help, а ещё умею здороваться.
Ты: приветик
Бот: И тебе привет! 👋
Ты: Цыплёнок, как дела?
Бот: Я пока умею немного, но скоро научусь большему 🐣Результат: в чате бот по-разному отвечает на команды /start и /help, отдельно ловит сообщения со словом «привет» и даёт ответ-заглушку на всё остальное.
Чтобы так получилось, нужно понять три вещи: что такое хэндлер, как его зарегистрировать через роутер и декоратор и как фильтры решают, кому достанется сообщение. Поехали.
Хэндлер — это официант за конкретным столиком
Вспомни аналогию из прошлого урока: бот — как официант, который бегает между тобой и кухней Telegram. Но в большом ресторане официант не один. У каждого свой участок: кто-то обслуживает столик у окна, кто-то — барную стойку, кто-то принимает только заказы на десерт.
Хэндлер — это и есть такой официант, закреплённый за конкретным типом сообщений. Один хэндлер обслуживает команду /start, другой — /help, третий — все сообщения, где встречается слово «привет». Когда приходит новое сообщение (то самое обновление, update), кто-то должен решить, какой официант им займётся. Этим занимается диспетчер.
Важно понять одну вещь, которая поначалу удивляет: ты сам нигде не вызываешь свои хэндлеры. Ты их только описываешь и регистрируешь, а вызывает их aiogram — в нужный момент, когда придёт подходящее сообщение. Это похоже на то, как ты вешаешь на дверь магазина табличку «звонок для курьеров»: ты не стоишь у двери и не бежишь открывать сам, ты заранее договорился, что на звонок выйдет нужный человек. Такой стиль программирования называют «событийным»: ты заранее раскладываешь реакции на разные события, а потом просто запускаешь бота и он живёт сам. Для новичка это и есть тот самый «страшный сервер» — на деле же это просто бесконечный цикл, который ловит сообщения и зовёт твои функции.
Dispatcher и Router: главный по залу и его помощники
В aiogram 3.x есть Dispatcher — объект, который принимает все входящие обновления и раздаёт их хэндлерам. Это главный администратор ресторана: он стоит у входа и направляет каждого гостя к нужному официанту.
Но складывать все хэндлеры прямо в диспетчер неудобно — представь администратора, который держит в голове сотню столиков. Поэтому в aiogram 3.x хэндлеры группируют в Router (роутер) — это как бригадир, который отвечает за свой участок зала. Ты создаёшь роутер, вешаешь на него хэндлеры, а потом подключаешь роутер к диспетчеру одной строчкой. Получается аккуратно: диспетчер знает про бригадиров, а бригадиры знают про официантов.
В маленьком боте роутер может быть один. В большом — по роутеру на каждый раздел: один для команд, другой для кнопок, третий для админки. Так код не превращается в кашу.
Первый хэндлер: команда /start через роутер
Возьмём наш bot.py из прошлого урока и перепишем его аккуратно — через роутер. Имена оставляем те же: объект bot, dp (диспетчер), токен берём из переменной окружения.
import asyncio
import os
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command
from aiogram.types import Message
router = Router()
@router.message(Command("start"))
async def cmd_start(message: Message):
await message.answer(
"Привет! Я Цыплёнок-помощник 🐤 "
"Напиши /help, чтобы узнать, что я умею."
)
async def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())Результат: в чате на сообщение /start бот ответит «Привет! Я Цыплёнок-помощник 🐤 Напиши /help, чтобы узнать, что я умею.». На остальные сообщения он пока никак не реагирует.
Разберём по шагам, что тут происходит:
router = Router()— создаём бригадира, который будет хранить наши хэндлеры.@router.message(...)— это декоратор. Строчка над функцией, которая говорит роутеру: «функция ниже — хэндлер, повесь её на сообщения». Сам по себе декоратор ничего не печатает — он просто регистрирует функцию.Command("start")внутри декоратора — это фильтр. Он сужает зону ответственности хэндлера: тот сработает только на команду/start, а не на каждое сообщение подряд.async def cmd_start(message: Message)— сам хэндлер. В него aiogram передаёт объектmessage— пришедшее сообщение со всеми данными: текст, автор, чат.await message.answer("...")— отправляем ответ в тот же чат.answer— это «ответить туда, откуда пришло сообщение».dp.include_router(router)— подключаем нашего бригадира к администратору-диспетчеру. Без этой строки хэндлеры просто не заработают: диспетчер о них не знает.
Обрати внимание: имя функции cmd_start не важно для Telegram — оно только для тебя. Можно назвать хоть obrabotchik_komandy_start, но короткое осмысленное имя удобнее.
Фильтр Command: ловим команды
Команда — это сообщение, которое начинается со слэша: /start, /help, /weather. Фильтр Command как раз про них. Добавим к боту вторую команду — /help:
@router.message(Command("help"))
async def cmd_help(message: Message):
await message.answer(
"Я понимаю команды /start и /help, "
"а ещё умею здороваться. Напиши мне «привет»!"
)Результат: в чате на /help бот ответит «Я понимаю команды /start и /help, а ещё умею здороваться. Напиши мне «привет»!».
Несколько важных деталей про Command:
- Слэш писать не нужно: пишешь
Command("help"), а Telegram сам сопоставит это с сообщением/help. Если напишешьCommand("/help")— фильтр сломается и команда ловиться не будет. - Одним фильтром можно ловить несколько команд-синонимов:
Command("help", "info")сработает и на/help, и на/info. Commandпонимает и команды с аргументами, например/weather Москва. Сам город ты достанешь позже из объекта сообщения — об этом будет отдельный урок.- Команда срабатывает, даже если после неё идёт «хвостик» с именем бота: в группах Telegram люди пишут
/help@my_chick_bot, чтобы было понятно, к какому именно боту обращаются. ФильтрCommandтакие хвосты понимает сам — тебе ничего дополнительно делать не нужно. Это пригодится, когда Цыплёнок в финале курса поселится в чате игрового клана и там окажется не один бот.
Почему вообще существует отдельный фильтр для команд, ведь команда — это тоже просто текст со слэшем? Потому что Telegram относится к командам по-особому: подсвечивает их синим, предлагает в подсказках, складывает в меню рядом с полем ввода. Command учитывает все эти тонкости за тебя и аккуратно отделяет имя команды от её аргументов. Пытаться ловить команды вручную через F.text можно, но это как открывать консервную банку ножом, когда рядом лежит открывашка.
Магический фильтр F: ловим обычные сообщения
Команды — это хорошо, но люди часто пишут боту просто текст: «привет», «спасибо», «как дела». Чтобы ловить такие сообщения по содержанию, в aiogram 3.x есть магический фильтр F. Его называют «магическим», потому что он позволяет коротко описывать условия прямо в декораторе, будто ты обращаешься к полям сообщения.
Представь F как указатель на пришедшее сообщение: F.text — это «текст сообщения», и ты можешь задавать ему условия. Импортируется F прямо из aiogram:
from aiogram import Bot, Dispatcher, Router, F
@router.message(F.text.lower().contains("привет"))
async def hello(message: Message):
await message.answer("И тебе привет! 👋")Результат: в чате на любое сообщение, в тексте которого есть слово «привет» (в любом регистре — «Привет», «ПРИВЕТ», «приветики»), бот ответит «И тебе привет! 👋».
Прочитай условие F.text.lower().contains("привет") вслух, как фразу: «возьми текст сообщения, приведи его к нижнему регистру и проверь, содержит ли он слово „привет“». Если да — хэндлер срабатывает. Это и есть магия F: ты описываешь условие почти как обычное предложение.
Вот ещё несколько полезных вариантов F, которые пригодятся уже скоро:
| Условие | Когда сработает |
F.text == "меню" | текст сообщения ровно «меню», без лишних слов |
F.text | пришёл любой текст (а не фото, стикер или голосовое) |
F.photo | пользователь прислал фотографию |
F.text.startswith("напомни") | сообщение начинается со слова «напомни» |
Маленький разбор: как «contains» работает на обычном Python
Магический F внутри делает примерно то же, что ты уже умеешь руками. Вот тот же приём проверки слова в строке на чистом Python — этот сниппет можно запустить:
text = "Приветики, цыплёнок!"
if "привет" in text.lower():
print("нашли приветствие")
else:
print("обычное сообщение")Вывод:
нашли приветствие
Видишь? text.lower() делает строку строчными буквами, а оператор in проверяет, есть ли внутри подстрока. Магический фильтр F.text.lower().contains("привет") — это та же логика, только записанная коротко и переданная роутеру заранее, чтобы он сам её проверил при каждом сообщении.
Порядок имеет значение: кто первый встал, того и сообщение
А теперь — самое важное и самое коварное. У нас уже несколько хэндлеров. Что будет, если сообщение подходит сразу под несколько фильтров? Например, кто-то написал боту слово «привет».
Правило простое: aiogram проверяет хэндлеры сверху вниз, в том порядке, в котором ты их зарегистрировал, и останавливается на первом подходящем. Как очередь в столовой: первый освободившийся повар берёт твой поднос, остальные даже не смотрят в твою сторону.
Добавим в самый конец хэндлер-заглушку, который ловит вообще всё:
@router.message()
async def fallback(message: Message):
await message.answer("Я пока умею немного, но скоро научусь большему 🐣")Результат: в чате на любое сообщение, которое не подошло ни под одну команду и не содержит «привет», бот ответит «Я пока умею немного, но скоро научусь большему 🐣».
Заметь: у этого хэндлера в декораторе @router.message() вообще нет фильтра — значит, он ловит всё подряд. Именно поэтому его место — в самом конце файла. Если поставить такую заглушку первой, она перехватит каждое сообщение, и до /start и приветствий очередь просто не дойдёт. Бот будет на всё отвечать «Я пока умею немного…», и ты будешь долго чесать затылок, почему команды «не работают».
Запомни порядок как лестницу: сначала самые узкие, конкретные хэндлеры (команды, точные совпадения), а самый широкий «лови-всё» — в самом низу.
Эта же логика спасает и в более тонких случаях. Допустим, у тебя есть отдельная реакция на точное «меню» (F.text == "меню") и реакция на любое сообщение, где встречается слово «меню» (F.text.lower().contains("меню")). Если поставить «содержит меню» выше «ровно меню», то точный вариант никогда не сработает — широкий перехватит его первым. Поэтому конкретное всегда идёт перед общим, точное совпадение — перед «содержит». Думай об этом как о сортировке: чем строже условие, тем выше оно в файле.
Маленький лайфхак на будущее: когда бот «ведёт себя странно» и отвечает не то, почти всегда дело не в сломанном коде хэндлера, а в его месте в файле. Первым делом проверяй порядок — это экономит часы.
Цыплёнок целиком: собираем все хэндлеры вместе
Чтобы порядок стал нагляднее, вот как выглядит наш bot.py, когда все хэндлеры собраны в один файл в правильной последовательности — от самых узких к самому широкому:
import asyncio
import os
from aiogram import Bot, Dispatcher, Router, F
from aiogram.filters import Command
from aiogram.types import Message
router = Router()
@router.message(Command("start"))
async def cmd_start(message: Message):
await message.answer("Привет! Я Цыплёнок-помощник 🐤 Напиши /help.")
@router.message(Command("help"))
async def cmd_help(message: Message):
await message.answer("Я понимаю /start и /help, а ещё умею здороваться.")
@router.message(F.text.lower().contains("привет"))
async def hello(message: Message):
await message.answer("И тебе привет! 👋")
@router.message()
async def fallback(message: Message):
await message.answer("Я пока умею немного, но скоро научусь большему 🐣")
async def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())Результат: в чате бот по-разному отвечает на /start и /help, ловит любое сообщение со словом «привет», а на всё остальное даёт ответ-заглушку. Именно тот сценарий, который мы наметили в начале урока.
Пробегись по файлу сверху вниз глазами aiogram: на каждое сообщение он по очереди примеряет фильтры cmd_start, cmd_help, hello, fallback — и останавливается на первом, который подошёл. Поскольку fallback стоит последним и без фильтра, он подхватывает всё, что не разобрали хэндлеры выше. Поменяй его местами с cmd_start — и бот тут же «оглохнет» на команды.
Частые ошибки новичков
- Забыл подключить роутер. Написал кучу хэндлеров, запустил бота — а он молчит. Скорее всего, нет строки
dp.include_router(router). Диспетчер не знает про твоего бригадира, и сообщения никому не достаются. - Слэш внутри Command.
Command("/start")со слэшем — частая ловушка. Внутри фильтра слэш не нужен, пишиCommand("start"). Со слэшем команда просто не будет ловиться. - Заглушка стоит слишком высоко. Если
@router.message()без фильтра оказался выше команд, он съест все сообщения, и команды «перестанут работать». Лекарство одно: широкий хэндлер всегда в самый низ. - Забыл await. Написал
message.answer("...")безawait— бот промолчит, а в консоли может мелькнуть предупреждение про «coroutine was never awaited». Отправка ответа в Telegram — асинхронная операция, её всегда нужно «дождаться» черезawait. - Хэндлер без async. Если объявить функцию как обычную
def hello(message):вместоasync def, aiogram не сможет её правильно вызвать. Все хэндлеры — асинхронные, всегдаasync def.
Мини-практика: научи Цыплёнка трём реакциям
Теперь твоя очередь. Возьми текущий bot.py и добавь Цыплёнку три новые реакции — так, чтобы порядок хэндлеров был правильным:
- Команда
/about— бот рассказывает о себе в одно предложение («Я учебный бот, меня пишет такой-то на aiogram»). - На сообщение со словом «спасибо» (в любом регистре) бот отвечает чем-то тёплым, например «Всегда пожалуйста! 🐤». Подсказка:
F.text.lower().contains("спасибо"). - На точный текст «меню» (ровно это слово) бот отвечает списком того, что умеет. Подсказка:
F.text == "меню".
Проверь себя: где в файле должны стоять эти три хэндлера относительно заглушки fallback? Правильный ответ — все три выше заглушки, потому что они конкретнее. А команда /about может стоять рядом с другими командами в любом порядке между собой — они не пересекаются по условиям, поэтому их взаимный порядок не важен.
Дополнительное задание для смелых: сделай так, чтобы бот реагировал и на «спасибо», и на «благодарю» одним хэндлером. Подумай, как объединить два условия (подсказка: в Python есть оператор |, и F его понимает: F.text.lower().contains("спасибо") | F.text.lower().contains("благодарю")).
Итоги
Сегодня наш «Цыплёнок-помощник» научился различать сообщения и отвечать на каждое по-своему. Главное, что стоит унести с собой:
- Хэндлер — функция-официант, закреплённая за определённым типом сообщений.
- Router группирует хэндлеры, а Dispatcher раздаёт им обновления; роутер подключают через
dp.include_router(router). - Декоратор
@router.message(...)регистрирует хэндлер, а фильтр внутри решает, на что он сработает. - Фильтр
Command("start")ловит команды (без слэша!), а магическийF— обычные сообщения по их содержимому. - Хэндлеры проверяются сверху вниз до первого подходящего, поэтому узкие — выше, «лови-всё» — в самом низу.
В следующем уроке мы копнём в сам объект message: научимся доставать из него имя пользователя, текст после команды и отвечать персонально — «Привет, Аня!» вместо безликого «Привет!». Цыплёнок станет внимательнее. До встречи! 🐣