Роутеры, многофайловость и структура бота¶
Используемая версия aiogram: 3.7.0
В этой главе мы познакомимся с новой фичей aiogram 3.x — роутерами, научимся разбивать наш код на отдельные компоненты, а также сформируем базовую структуру бота, которая пригодится в следующих главах и вообще по жизни.
Точка входа в приложение¶
Театр начинается с вешалки, а бот начинается с точки входа. Пусть это будет файл bot.py
. В нём мы определим
асинхронную функцию main()
, в которой создадим необходимые объекты и запустим поллинг. Какие
объекты являются необходимыми? Во-первых, разумеется, бот. Их может быть несколько, но об этом
как-нибудь в другой раз. Во-вторых, диспетчер. Он занимается приёмом событий от Telegram и раскидыванием их
по хэндлерам через фильтры и мидлвари.
import asyncio
from aiogram import Bot, Dispatcher
# Запуск бота
async def main():
bot = Bot(token="TOKEN")
dp = Dispatcher()
# Запускаем бота и пропускаем все накопленные входящие
# Да, этот метод можно вызвать даже если у вас поллинг
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Но чтобы обрабатывать сообщения, этого недостаточно, нужны ещё хэндлеры. Мы хотим их расположить
в других файлах, чтобы не устраивать портянки на несколько тысяч строк. В предыдущих главах все
наши хэндлеры прицеплялись к диспетчеру, но сейчас он внутри функции и мы точно не хотим
делать его глобальным объектом.
Что же делать? И тут на помощь приходят...
Роутеры¶
Обратимся к официальной документации aiogram 3.x и посмотрим на следующее изображение:
Что мы видим?
- Диспетчер — корневой роутер.
- Хэндлеры цепляются к роутерам.
- Роутеры могут быть вложенными, но между ними только однонаправленная связь.
- Порядок включения (и, соответственно, проверки) роутеров явно определён.
На следующем изображении виден порядок поиска апдейтом нужного хэндлера для выполнения:
Напишем простенького бота с двумя фичами:
- Если боту отправили
/start
, он должен прислать вопрос и две кнопки с текстами «Да» и «Нет». - Если боту прислали любой другой текст, стикер или гифку, он должен ответить названием типа сообщения.
Начнём с клавиатуры: создадим рядом с файлом bot.py
каталог keyboards
, а внутри него файл for_questions.py
и напишем функцию для получения простой клавиатуры с кнопками "Да" и "Нет" в один ряд:
from aiogram.types import ReplyKeyboardMarkup
from aiogram.utils.keyboard import ReplyKeyboardBuilder
def get_yes_no_kb() -> ReplyKeyboardMarkup:
kb = ReplyKeyboardBuilder()
kb.button(text="Да")
kb.button(text="Нет")
kb.adjust(2)
return kb.as_markup(resize_keyboard=True)
Ничего сложного, тем более, что мы клавиатуры подробно разбирали ранее.
Теперь рядом с файлом bot.py
создадим другой каталог handlers
, а внутри него файл questions.py
.
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, ReplyKeyboardRemove
from keyboards.for_questions import get_yes_no_kb
router = Router() # [1]
@router.message(Command("start")) # [2]
async def cmd_start(message: Message):
await message.answer(
"Вы довольны своей работой?",
reply_markup=get_yes_no_kb()
)
@router.message(F.text.lower() == "да")
async def answer_yes(message: Message):
await message.answer(
"Это здорово!",
reply_markup=ReplyKeyboardRemove()
)
@router.message(F.text.lower() == "нет")
async def answer_no(message: Message):
await message.answer(
"Жаль...",
reply_markup=ReplyKeyboardRemove()
)
Обратим внимание на пункты [1] и [2]. Во-первых, мы в файле создали свой собственный роутер уровня модуля, и далее будем цеплять его к корневому роутеру (диспетчеру). Во-вторых, хэндлеры «отпочковываются» уже от локального роутера.
Аналогичным образом сделаем второй файл с хэндлерами different_types.py
, где просто будем выводить тип сообщения:
from aiogram import Router, F
from aiogram.types import Message
router = Router()
@router.message(F.text)
async def message_with_text(message: Message):
await message.answer("Это текстовое сообщение!")
@router.message(F.sticker)
async def message_with_sticker(message: Message):
await message.answer("Это стикер!")
@router.message(F.animation)
async def message_with_gif(message: Message):
await message.answer("Это GIF!")
Наконец, вернёмся к нашему bot.py
, импортируем файлы с роутерами и хэндлерами, и подключим их к диспетчеру:
import asyncio
from aiogram import Bot, Dispatcher
from handlers import questions, different_types
# Запуск бота
async def main():
bot = Bot(token="TOKEN")
dp = Dispatcher()
dp.include_routers(questions.router, different_types.router)
# Альтернативный вариант регистрации роутеров по одному на строку
# dp.include_router(questions.router)
# dp.include_router(different_types.router)
# Запускаем бота и пропускаем все накопленные входящие
# Да, этот метод можно вызвать даже если у вас поллинг
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Мы просто импортируем файлы из каталога handlers/
и подключаем роутеры из этих файлов к диспетчеру. И здесь снова
важен порядок импортов! Если мы поменяем местами регистрацию роутеров, то на команду /start
бот будет отвечать
фразой «Это текстовое сообщение!», поскольку функция message_with_text()
первой успешно пройдёт все фильтры. Но
о самих фильтрах мы поговорим чуть позже, а пока рассмотрим ещё один вопрос.
Итог¶
У нас получилось аккуратно разделить бота по разным файлам, не нарушая его работу. Примерное дерево файлов и каталогов получилось следующим (здесь сознательно пропущены некоторые несущественные для примера файлы):
├── bot.py
├── handlers
│ ├── different_types.py
│ └── questions.py
├── keyboards
│ └── for_questions.py
В дальнейшем мы будем придерживаться такой структуры, плюс добавятся новые каталоги для фильтров, мидлварей, файлов для работы с базами данных и т.д.