MAATRIX / Блог / Aiogram 3: основы создания Telegram-бота

Aiogram 3: основы создания Telegram-бота

Aiogram 3: основы создания Telegram-бота

MAATRIX

Если вы искали, с чего начать писать Telegram-бота на Python в 2026 году, ответ почти наверняка — Aiogram 3. Библиотека полностью асинхронная, разработчики активно её поддерживают, а старые примеры под Aiogram 2 из интернета работать не будут — синтаксис поменялся заметно. Дальше — рабочий минимум: от установки до бота, который переживёт перезагрузку сервера.

Чем Aiogram 3 отличается от Aiogram 2

Разница не косметическая, это по сути другой фреймворк с похожим названием.

Router вместо Dispatcher как держателя хендлеров. В Aiogram 2 весь код регистрировался в одном Dispatcher через декораторы @dp.message_handler(...). В Aiogram 3 хендлеры вешаются на объект Router, а роутеры потом подключаются к Dispatcher через dp.include_router(). Это позволяет разбить бота на модули: один роутер на регистрацию, другой на админку, третий на платежи — и каждый живёт в своём файле, не завися от глобального объекта диспетчера.

FSM (машина состояний) переехала в middleware. В Aiogram 2 состояния хранились через state_storage и подключались к диспетчеру напрямую. В третьей версии FSM работает как встроенный middleware поверх Dispatcher, а хранилище состояний (MemoryStorage, RedisStorage) передаётся при создании диспетчера. Логика та же — состояния и переходы между ними, но код чище.

Явные фильтры вместо метода filters=[...]. В Aiogram 3 фильтрация построена вокруг magic-фильтра F (аналог field из aiogram.types) и класса Filter. Синтаксис F.text == "привет" читается почти как обычный Python и не требует лямбд.

Полная типизация и Pydantic. Все объекты API (Message, CallbackQuery, User) — типизированные Pydantic-модели. IDE подсказывает поля, автодополнение работает без сюрпризов.

Требования к окружению. Aiogram 3 формально работает от Python 3.8, но на практике разумно ставить 3.11 или новее — часть зависимостей рассчитана на актуальные релизы Python. Точную минимальную версию Python и номер текущего релиза Aiogram 3 сверяйте командой pip index versions aiogram перед установкой — библиотека обновляется, и любые зафиксированные здесь цифры быстро устареют.

Если раньше писали на Aiogram 2 и переносите старого бота — перепишите заново, а не патчите: различий в архитектуре слишком много, экономии времени не получится.

Установка и подготовка окружения

Ставить Aiogram 3 в системный Python — плохая идея, зависимости бота быстро начнут конфликтовать с другими проектами на сервере. Работаем через виртуальное окружение.

mkdir mybot && cd mybot
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install aiogram python-dotenv

python-dotenv не обязателен, но токен бота хранить в коде — плохая практика: файл рано или поздно попадёт в git или в публичный репозиторий. Создайте .env:

BOT_TOKEN=1234567890:AAExampleTokenNotReal

И добавьте .env в .gitignore, если используете git.

Токен получают у @BotFather в самом Telegram: команда /newbot, имя бота, username, оканчивающийся на bot. В ответ приходит токен вида <id>:<хэш> — он и нужен в .env.

Проверить, что всё установилось, можно так:

python -c "import aiogram; print(aiogram.__version__)"

Если версия начинается на 3. — окружение готово.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Минимальный рабочий бот

Самый простой рабочий бот — один файл, один хендлер на команду /start и один на эхо любого текста.

# main.py
import asyncio
import logging
import os

from aiogram import Bot, Dispatcher, Router
from aiogram.filters import CommandStart
from aiogram.types import Message
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(level=logging.INFO)

router = Router()


@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
    await message.answer(f"Привет, {message.from_user.full_name}!")


@router.message()
async def echo(message: Message) -> None:
    await message.answer(message.text or "Это не текст, я умею отвечать только на текст")


async def main() -> None:
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Запуск:

python main.py

Обратите внимание на порядок хендлеров в роутере: @router.message() без фильтров ловит вообще всё, поэтому его нужно регистрировать последним — иначе он перехватит апдейты раньше специфичных хендлеров вроде cmd_start.

Здесь используется start_polling — бот сам опрашивает Telegram API каждые несколько секунд. Это самый простой способ запуска и для старта на VPS его достаточно; про альтернативу через вебхуки и когда она оправдана — в статье про webhook и polling.

Фильтры: как отбирать нужные апдейты

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

Встроенные фильтры для команд:

from aiogram.filters import Command, CommandStart

@router.message(CommandStart())
async def start(message: Message): ...

@router.message(Command("help"))
async def help_cmd(message: Message): ...

@router.message(Command("price", "cost"))  # несколько вариантов сразу
async def price(message: Message): ...

Magic-фильтр F — самый гибкий инструмент, работает как обёртка над полями объекта:

from aiogram import F

@router.message(F.text.contains("сервер"))
async def about_server(message: Message):
    await message.answer("Расскажу про аренду VPS")

@router.message(F.photo)
async def on_photo(message: Message):
    await message.answer("Фото получил")

@router.callback_query(F.data == "confirm")
async def on_confirm(callback):
    await callback.answer("Подтверждено")

Можно комбинировать через & и |:

@router.message(F.text & F.chat.type == "private")
async def private_text(message: Message): ...

Свои фильтры пишутся как класс, наследующий Filter, если условие сложнее одной строки — например, проверка роли пользователя в базе. Для простого бота обычно хватает Command и F.

FSM: состояния для многошаговых сценариев

Когда бот должен последовательно спросить несколько вещей (например, имя, потом телефон, потом подтверждение) — используется FSM (Finite State Machine).

from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage


class OrderForm(StatesGroup):
    name = State()
    phone = State()


@router.message(Command("order"))
async def start_order(message: Message, state: FSMContext):
    await state.set_state(OrderForm.name)
    await message.answer("Как вас зовут?")


@router.message(StateFilter(OrderForm.name))
async def process_name(message: Message, state: FSMContext):
    await state.update_data(name=message.text)
    await state.set_state(OrderForm.phone)
    await message.answer("Телефон для связи?")


@router.message(StateFilter(OrderForm.phone))
async def process_phone(message: Message, state: FSMContext):
    data = await state.update_data(phone=message.text)
    await state.clear()
    await message.answer(f"Записал: {data['name']}, {data['phone']}")

Хранилище состояний передаётся при создании диспетчера:

dp = Dispatcher(storage=MemoryStorage())

MemoryStorage держит состояния в оперативной памяти процесса — просто, но при перезапуске бота все незавершённые сценарии теряются. Для продакшена, где бот может перезапускаться (обновление кода, systemd-рестарт), логичнее RedisStorage — состояния переживут рестарт процесса, если Redis работает отдельно. Это требует поднятого Redis на сервере — как его развернуть, описано в статье про установку Redis на VPS.

Структура проекта для бота посерьёзнее

Один файл main.py нормален для прототипа, но как только хендлеров становится десяток-другой, лучше разложить код по модулям.

mybot/
├── venv/
├── .env
├── main.py
├── config.py
├── handlers/
│   ├── __init__.py
│   ├── start.py
│   ├── order.py
│   └── admin.py
├── keyboards/
│   ├── __init__.py
│   └── main_menu.py
├── middlewares/
│   └── throttling.py
└── requirements.txt

config.py — чтение переменных окружения в одном месте:

import os
from dataclasses import dataclass
from dotenv import load_dotenv

load_dotenv()


@dataclass
class Config:
    bot_token: str
    admin_id: int


def load_config() -> Config:
    return Config(
        bot_token=os.environ["BOT_TOKEN"],
        admin_id=int(os.environ["ADMIN_ID"]),
    )

Каждый модуль в handlers/ заводит свой Router и экспортирует его:

# handlers/start.py
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message

router = Router(name="start")


@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
    await message.answer("Привет! Это стартовый роутер.")

А main.py только собирает всё вместе:

import asyncio
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage

from config import load_config
from handlers import start, order, admin


async def main() -> None:
    config = load_config()
    bot = Bot(token=config.bot_token)
    dp = Dispatcher(storage=MemoryStorage())

    dp.include_router(start.router)
    dp.include_router(order.router)
    dp.include_router(admin.router)

    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

Такая структура держится годами без переписывания — новый функционал добавляется новым файлом в handlers/, а не правкой одного разросшегося main.py.

Запуск как systemd-сервис на VPS

python main.py в терминале умирает вместе с сессией SSH. Для постоянной работы бот нужно оформить как systemd-сервис — тогда он переживёт разрыв соединения, перезапустится сам после сбоя и поднимется автоматически после перезагрузки сервера.

Сначала зафиксируйте зависимости:

pip freeze > requirements.txt

Создайте юнит-файл:

sudo nano /etc/systemd/system/mybot.service
[Unit]
Description=Telegram bot on aiogram 3
After=network.target

[Service]
Type=simple
User=botuser
WorkingDirectory=/home/botuser/mybot
ExecStart=/home/botuser/mybot/venv/bin/python /home/botuser/mybot/main.py
Restart=on-failure
RestartSec=5
EnvironmentFile=/home/botuser/mybot/.env

[Install]
WantedBy=multi-user.target

Запускать бота от отдельного непривилегированного пользователя (botuser), а не от root — стандартная гигиена для любого сервиса на сервере.

sudo systemctl daemon-reload
sudo systemctl enable mybot
sudo systemctl start mybot
sudo systemctl status mybot

Логи смотрите через:

journalctl -u mybot -f

После правки кода не забывайте sudo systemctl restart mybot — иначе процесс продолжит работать со старой версией файла в памяти. Если бот падает сразу после старта и рестартует по кругу, разбор типичных причин есть в статье про бота, который падает после перезапуска.

Для polling важна не столько мощность сервера, сколько его непрерывная доступность. Сколько ресурсов реально нужно под такого бота — отдельная тема, разобрана в статье сколько RAM нужно для Telegram-бота.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Aiogram 3 совместим с кодом на Aiogram 2?

Нет, API изменился достаточно сильно (Router, синтаксис фильтров, работа с FSM), старый код нужно переписывать, а не адаптировать точечно.

Обязательно ли использовать FSM для простого бота?

Нет. Если у бота нет многошаговых диалогов — достаточно хендлеров с фильтрами Command и F, FSM усложнит код без пользы.

Что выбрать для хранения FSM-состояний в продакшене — Memory или Redis?

MemoryStorage подходит для разработки и небольших ботов без критичных сценариев. Если бот перезапускается автоматически (например, по расписанию или после деплоя) и важно не терять незавершённые диалоги пользователей — берите RedisStorage.

Можно ли запускать несколько ботов на одном VPS?

Да, каждый бот — отдельный процесс со своим systemd-юнитом и своим виртуальным окружением; они не мешают друг другу, если не конкурируют за одни и те же порты (актуально только для вебхуков).

Polling или webhook — с чего начинать?

Начинайте с polling — он проще в настройке и не требует домена с SSL. Переходить на вебхуки стоит, когда важна задержка ответа или бот обслуживает заметный поток сообщений.