Хэндлеры сообщений и команд

Хэндлер — это функция, которую 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, чтобы узнать, что я умею.». На остальные сообщения он пока никак не реагирует.

Разберём по шагам, что тут происходит:

  1. router = Router() — создаём бригадира, который будет хранить наши хэндлеры.
  2. @router.message(...) — это декоратор. Строчка над функцией, которая говорит роутеру: «функция ниже — хэндлер, повесь её на сообщения». Сам по себе декоратор ничего не печатает — он просто регистрирует функцию.
  3. Command("start") внутри декоратора — это фильтр. Он сужает зону ответственности хэндлера: тот сработает только на команду /start, а не на каждое сообщение подряд.
  4. async def cmd_start(message: Message) — сам хэндлер. В него aiogram передаёт объект message — пришедшее сообщение со всеми данными: текст, автор, чат.
  5. await message.answer("...") — отправляем ответ в тот же чат. answer — это «ответить туда, откуда пришло сообщение».
  6. 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 и добавь Цыплёнку три новые реакции — так, чтобы порядок хэндлеров был правильным:

  1. Команда /about — бот рассказывает о себе в одно предложение («Я учебный бот, меня пишет такой-то на aiogram»).
  2. На сообщение со словом «спасибо» (в любом регистре) бот отвечает чем-то тёплым, например «Всегда пожалуйста! 🐤». Подсказка: F.text.lower().contains("спасибо").
  3. На точный текст «меню» (ровно это слово) бот отвечает списком того, что умеет. Подсказка: 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: научимся доставать из него имя пользователя, текст после команды и отвечать персонально — «Привет, Аня!» вместо безликого «Привет!». Цыплёнок станет внимательнее. До встречи! 🐣

Проверьте себя
1. Что делает строка @router.message(Command("start")) над функцией?
AСразу отправляет пользователю команду /start
BРегистрирует функцию как хэндлер, который сработает на команду /start
CСоздаёт новую команду в меню Telegram
DЗапускает бота на сервере
2. Бот написан правильно, но на команды совсем не реагирует. Какой строки, скорее всего, не хватает?
Aimport asyncio
Bawait message.answer(...)
Cdp.include_router(router)
Dasyncio.run(main())
3. Как правильно записать фильтр для команды /help?
ACommand("/help")
BCommand("help")
CF.help
Dmessage("help")
4. Почему хэндлер с пустым декоратором @router.message() (без фильтра) нужно ставить в самом конце?
AТак требует синтаксис Python
BОн ловит любые сообщения, и если поставить его выше, он перехватит всё до команд и точных совпадений
CИначе бот не запустится
DПустой декоратор работает медленнее остальных
5. Что проверяет условие F.text.lower().contains("привет")?
AЧто сообщение — это команда /привет
BЧто текст сообщения ровно равен слову «привет»
CЧто в тексте сообщения (в любом регистре) есть слово «привет»
DЧто пользователь прислал фотографию
6. Каким должен быть хэндлер по объявлению функции в aiogram 3.x?
AОбычной функцией: def hello(message):
BАсинхронной функцией: async def hello(message):
CМетодом класса Bot
DЛюбой функцией без аргументов