Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 8 из 10 Средний 40 мин 100 XP

Middleware в aiogram: антиспам, логирование и общие данные

Пишем middleware в aiogram: антиспам-троттлинг, логирование всех апдейтов и передачу общих данных в хендлеры — механику запускаем прямо на странице.

Редакция Питоники

Ваш бот из урока про обработчики оброс командами, из урока про асинхронность он научился не замирать — и вот в него пришли люди. Все сразу. Пятнадцать сообщений в секунду из одного чата, рассылка спама, попытки уронить /calc строкой «+++». Хендлеры тут не помогут: они про «что отвечать», а вам нужен код про «пропускать ли вообще». Такой код называется middleware — и это последний архитектурный инструмент aiogram, которого у вас ещё нет.

В этом уроке разберём механику на чистом Python — она запускается прямо на странице, — а затем покажем настоящий код на aiogram 3 с классом BaseMiddleware. Соберём три классических middleware: троттлинг-антиспам, логирование апдейтов и передачу общих данных в хендлеры.

Что такое middleware и чем он отличается от хендлера?

Middleware — это код, который стоит между апдейтом и хендлером: он видит каждое сообщение раньше вашего обработчика. Диспетчер aiogram получает апдейт от Telegram и ведёт его по цепочке: сначала middleware, потом фильтры, потом хендлер. Middleware может пропустить апдейт дальше, изменить его, пополнить словарь данных — или остановить цепочку, и тогда хендлер даже не узнает, что что-то приходило.

ХендлерMiddleware
Сколько раз срабатываеттолько на подходящие сообщенияна каждый апдейт своей категории
Про что отвечаетчто ответить пользователюпропустить ли апдейт и что ему дать с собой
Сколько нужнодесятки на ботаединицы на бота
Примеры/start, /calc, тексттроттлинг, логи, подключение к базе

Правило большое: хендлеры — про бизнес-логику, middleware — про политику. «Считать сумму двух чисел» — хендлер. «Не больше одного сообщения в полсекунды от одного человека» — middleware. Если смешать их, антиспам-проверки расползутся по каждому хендлеру, и добавить новую проверку будет значит переписать половину бота.

  • Троттлинг — не давать одному пользователю завалить бота (сегодняшняя тема);
  • Логирование — записывать каждый апдейт до того, как его увидят хендлеры;
  • Общие данные — положить в data подключение к базе, конфиг, сессию — один раз на весь бот;
  • Аутентификация — пускать только пользователей из белого списка, остальным сразу отказ;
  • Язык и локаль — определить язык пользователя один раз, а не в каждом хендлере.

Как устроен лук middleware на чистом Python?

Внутри aiogram нет никакой магии — есть функции-обёртки. Обёртка получает хендлер и возвращает новый хендлер, который делает что-то до и после вызова оригинала. Так рождается «лук»: каждый слой middleware заворачивает всё, что внутри него. Запустите и посмотрите:

первый middleware: обёртка с логом
def with_logging(handler):
    def wrapper(event):
        print("-> апдейт:", event["text"])
        result = handler(event)
        print("<- ответ отправлен")
        return result
    return wrapper

def handle_message(event):
    return "Эхо: " + event["text"]

handler = with_logging(handle_message)
print(handler({"user_id": 1, "text": "привет"}))
Вывод
-> апдейт: привет
<- ответ отправлен
Эхо: привет

Разберём по строчкам. with_logging(handler) — фабрика обёрток: она не выполняет ничего сама, а возвращает новую функцию wrapper. Именно wrapper становится обработчиком: сначала печатает строку до, потом вызывает настоящий handler(event), потом печатает строку после — и не забывает вернуть результат. Пока это просто логирование, но на этом каркасе строится весь антиспам: между «до» и «после» можно поставить любую проверку.

А теперь наденем на хендлер два слоя сразу и посмотрим на порядок срабатывания:

два middleware: порядок слоёв лука
def logging_mw(handler):
    def wrapper(event, data):
        print("[лог] апдейт от", event["user_id"])
        return handler(event, data)
    return wrapper

def throttling_mw(handler):
    def wrapper(event, data):
        print("[антиспам] проверяю частоту")
        return handler(event, data)
    return wrapper

def handle(event, data):
    print("[хендлер] отвечаю на:", event["text"])
    return "Готово"

pipeline = logging_mw(throttling_mw(handle))
print(pipeline({"user_id": 42, "text": "/start"}, {}))
Вывод
[лог] апдейт от 42
[антиспам] проверяю частоту
[хендлер] отвечаю на: /start
Готово

Смотрите, что произошло: logging_mw(throttling_mw(handle)) — первый зарегистрированный middleware оказался снаружи. Выполнение идёт сверху вниз: лог, потом антиспам, потом хендлер. Это и есть конвейер aiogram — регистрация в коде бота означает ровно вот это заворачивание в лук. Порядок важен: лог хочется видеть даже для отброшенного спама, поэтому лог-слой ставят раньше троттлинга.

Как защитить бота от флуда: троттлинг-антиспам?

Флуд — это когда один пользователь шлёт сообщения быстрее, чем бот успевает их осмысленно обрабатывать. Классическое решение — троттлинг: ограничение частоты. Для каждого пользователя храним время последнего пропущенного сообщения; новое сообщение проходит, только если прошло больше лимита. Отдельный нюанс: у самого Telegram есть собственные лимиты, и ваш троттлинг — про то, чтобы до них дело не доходило. Живому человеку задержка в полсекунды незаметна, а ботоферме вы говорите «нет» ещё на входе. Вот троттлинг на чистом Python — время приходит внутри апдейта, значения фиксированные, чтобы вывод был воспроизводим:

троттлинг: не чаще одного сообщения в секунду
# Словарь user_id -> время последнего сообщения (как у настоящего троттлинга)
last_seen = {}
LIMIT = 1.0  # минимум одна секунда между сообщениями

def throttle(handler):
    def wrapper(event):
        user = event["user_id"]
        prev = last_seen.get(user)
        if prev is not None and event["time"] - prev < LIMIT:
            print("ФЛУД: пропущено от", user)
            return None  # хендлер даже не вызывается
        last_seen[user] = event["time"]
        return handler(event)
    return wrapper

def handle(event):
    print("Хендлер обработал сообщение от", event["user_id"])

pipeline = throttle(handle)

# время приходит вместе с апдейтом; значения фиксированные, из теста
pipeline({"user_id": 1, "time": 100.5})
pipeline({"user_id": 1, "time": 100.8})
pipeline({"user_id": 1, "time": 102.0})
pipeline({"user_id": 2, "time": 100.6})
Вывод
Хендлер обработал сообщение от 1
ФЛУД: пропущено от 1
Хендлер обработал сообщение от 1
Хендлер обработал сообщение от 2

Три вещи, на которые стоит посмотреть. Первая: last_seen.get(user) вместо last_seen[user] — нового пользователя в словаре ещё нет, и get честно вернёт None вместо падения с KeyError. Вторая: флуд-сообщение не роняет бота и не отвечает ошибкой — оно просто не доходит до хендлера (return None). Третья: пользователь номер 2 со своим временем проходит свободно — словарь разделяет пользователей, у каждого свой лимит.

Теперь тот же троттлинг в настоящем aiogram. Класс наследуется от BaseMiddleware и реализует асинхронный метод __call__ с той же самой сигнатурой, которую вы только что видели у wrapper: хендлер, апдейт, словарь data.

настоящий aiogram: класс ThrottlingMiddleware
import time
from collections.abc import Awaitable, Callable
from typing import Any

from aiogram import BaseMiddleware
from aiogram.types import TelegramObject


class ThrottlingMiddleware(BaseMiddleware):
    def __init__(self, limit: float = 0.5):
        self.limit = limit
        self.last_seen: dict[int, float] = {}

    async def __call__(
        self,
        handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
        event: TelegramObject,
        data: dict[str, Any],
    ) -> Any:
        user = event.from_user
        if user is None:  # апдейты из каналов без автора
            return await handler(event, data)
        now = time.monotonic()
        prev = self.last_seen.get(user.id)
        if prev is not None and now - prev < self.limit:
            return  # тихо пропускаем флуд, хендлер не вызывается
        self.last_seen[user.id] = now
        return await handler(event, data)


dp.message.middleware(ThrottlingMiddleware(limit=0.5))
Настоящий aiogram: без сети из браузерной песочницы его не запустить. Отличия от тренажёра выше — только три: метод асинхронный (async def __call__), хендлер вызывается через await, а сам класс регистрируется в диспетчере строкой dp.message.middleware(...). Логика — буква в букву ваша.

Регистрация dp.message.middleware(...) вешает класс на обычные сообщения. У aiogram есть и второй способ — outer_middleware, который срабатывает ещё раньше, до фильтров:

регистрация: inner и outer middleware
from aiogram import Dispatcher

dp = Dispatcher()

# outer: до фильтров, на каждый апдейт - сюда ставят логи и общие данные
dp.update.outer_middleware(LogMiddleware())
# inner: после фильтров, только когда хендлер нашёлся - сюда ставят троттлинг
dp.message.middleware(ThrottlingMiddleware(limit=0.5))
Фрагмент настройки диспетчера, а не запускаемая программа: показываем, где и в каком порядке middleware регистрируются в настоящем боте.

Почему троттлинг — inner, а логи — outer? Логи нужны на каждый апдейт, даже на тот, что не совпал ни с одним фильтром. А троттлинг незачем гонять лишний раз: если всё равно нет хендлера, который отвечал бы на такой апдейт, — спамить нечего. Тонкость на будущее: time.monotonic() выбрано не случайно — эта шкала не прыгает назад при переводе часов, в отличие от time.time().

Как передавать общие данные в хендлеры через словарь data?

Вы уже заметили второй аргумент обёрток — словарь data. Это чемодан, который middleware набивают полезным грузом, а хендлер потом открывает. Хендлеру нужна база данных? Конфиг с токенами? Имя пользователя? Middleware кладёт это в data один раз — и каждый хендлер получает готовое, не зная, откуда оно взялось. Почему бы не завести глобальную переменную с базой? На живом боте это стреляет при тестах: глобал один, а окружений много — тестовое, боевое, локальное. Словарь data собирается заново на каждый апдейт, и хендлер получает ровно то, что нужно в этом окружении:

передача данных в хендлер через словарь data
USERS = {7: "Анна", 42: "Борис"}

def inject_user(handler):
    def wrapper(event, data):
        # middleware обогащает data: хендлеру не нужно знать про USERS
        data["username"] = USERS.get(event["user_id"], "гость")
        data["is_new"] = event["user_id"] not in USERS
        return handler(event, data)
    return wrapper

def handle(event, data):
    if data["is_new"]:
        print(f"Рады знакомству, {data['username']}!")
    else:
        print(f"С возвращением, {data['username']}!")
    return "ok"

pipeline = inject_user(handle)
pipeline({"user_id": 7}, {})
pipeline({"user_id": 99}, {})
Вывод
С возвращением, Анна!
Рады знакомству, гость!

Хендлер handle не знает ни про словарь USERS, ни про то, как определяется новый пользователь. Он просто читает data["username"] — а подготовку делает middleware. В настоящем aiogram это превращается в элегантную вещь: всё, что middleware положил в data, диспетчер подставляет в хендлер как именованный аргумент.

настоящий aiogram: данные из data становятся аргументами хендлера
class DataContextMiddleware(BaseMiddleware):
    def __init__(self, db_path: str):
        self.db_path = db_path

    async def __call__(self, handler, event, data):
        data["db_path"] = self.db_path  # кладём в чемодан...
        return await handler(event, data)

dp.update.outer_middleware(DataContextMiddleware("expenses.db"))


@router.message(Command("report"))
async def cmd_report(message: Message, db_path: str):
    # ...и aiogram сам подставит db_path как аргумент хендлера
    ...
Фрагмент настоящего бота: без сети его не выполнить. Это встроенный механизм внедрения зависимостей aiogram - то, ради чего data существует. В уроке про SQLite тем же способом удобно передавать подключение к базе.

Как залогировать все апдейты одним middleware?

Логирование — самый простой и самый полезный middleware. Пять строк, а экономят часы: видно, какие апдейты приходят, от кого и что бот с ними сделал. Ошибки при этом ловить должен не лог-слой — для этого в следующем уроке есть отдельные инструменты:

настоящий aiogram: LogMiddleware
import logging

from aiogram import BaseMiddleware

log = logging.getLogger("bot.updates")


class LogMiddleware(BaseMiddleware):
    async def __call__(self, handler, event, data):
        user = event.from_user
        who = user.id if user else "канал"
        log.info("апдейт от %s, текст: %r", who, event.text)
        return await handler(event, data)
Показан только класс: где взять log и как настроить формат логов - тема следующего урока про обработку ошибок.

Что дальше?

Подведём итог. Middleware — обёртка над хендлером, которая срабатывает на каждый апдейт; лук из обёрток вы собрали руками. Троттлинг хранит время последнего сообщения в словаре и молча глушит флуд. Словарь data — чемодан, из которого хендлеры достают общие данные, не зная, откуда они взялись. Лог-слой пишет каждый апдейт до того, как его увидит любой хендлер.

Остался последний инструмент надёжности: обработка ошибок. В уроке 9 разберём try/except в хендлерах, лимиты Telegram с исключением RetryAfter, модуль logging вместо print и перезапуск бота, который переживает падение. А затем всё соберётся в финальный проект — бота-трекер расходов. Если хочется освежить, как хендлеры и фильтры различают команды, — вернитесь ко второму уроку.

Хендлеры отвечают за то, что бот говорит. Middleware — за то, кого бот слушает.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

def mw_a(handler):
    def wrapper(event):
        print("A-до")
        result = handler(event)
        print("A-после")
        return result
    return wrapper

def mw_b(handler):
    def wrapper(event):
        print("B-до")
        result = handler(event)
        print("B-после")
        return result
    return wrapper

def handle(event):
    print("ХЕНДЛЕР")

pipeline = mw_a(mw_b(handle))
pipeline("апдейт")
last_seen = {5: 10.0}
LIMIT = 2.0

def handle(event):
    print("Обработано от", event["user_id"])

user = 5
now = 11.5
prev = last_seen.get(user)
if now - prev < LIMIT:
    print("ФЛУД: пропущено")
else:
    last_seen[user] = now
    handle({"user_id": user, "time": now})
Проверь себя
0 / 5

1. Чем middleware принципиально отличается от хендлера?

2. В каком порядке сработают слои в цепочке logging_mw(throttling_mw(handle))?

3. Как троттлинг-middleware останавливает флуд-сообщение?

4. Хендлер объявлен как async def cmd_report(message: Message, db_path: str). Откуда возьмётся db_path?

5. Почему для троттлинга выбран time.monotonic(), а не time.time()?

Карточки терминов
Запомнено: 0 / 6
Практика

Напишите middleware-цензуру для бота-модератора: обёртка censor_mw должна проверять текст апдейта, и если в нём встречается слово «спам» (в любом регистре) — не вызывать хендлер, а вернуть строку «Удалено модератором». Остальные сообщения идут в хендлер как обычно.

practice.py
Вопросы и ответы по уроку

Что такое middleware в aiogram простыми словами?

Это код-посредник, который диспетчер запускает на каждом сообщении до хендлера: middleware может пропустить апдейт дальше, что-то ему добавить или отбросить, не вызывая обработчик вовсе. Через middleware делают антиспам, логирование и передачу общих данных (базы, конфига) в хендлеры.

Как защитить телеграм-бота от спама и флуда?

Ставят троттлинг-middleware: для каждого пользователя хранится время последнего сообщения, и если новое пришло раньше лимита (например, чаще раза в полсекунды), оно молча отбрасывается — хендлер не вызывается. В aiogram для этого пишут класс-наследник BaseMiddleware с методом __call__ и регистрируют его через dp.message.middleware(...).

Чем middleware отличается от хендлера в aiogram?

Хендлер отвечает на конкретный тип сообщения — команду, текст, callback — и пишет бизнес-логику. Middleware срабатывает на каждый апдейт до того, как фильтры выберут хендлер, и решает инфраструктурные вопросы: пропускать ли сообщение, что логировать, какие данные передать. Хендлеров в боте десятки, middleware — единицы.

Как передать базу данных или конфиг в обработчик aiogram?

Через middleware и словарь data: outer-middleware при старте кладёт подключение в data, а aiogram автоматически подставляет его именованным аргументом в каждый хендлер, который этот аргумент объявил. Никаких глобальных переменных — зависимости приходят явно.

Понравился урок? Сошлитесь на него

«Middleware — это код, который стоит между апдейтом и хендлером: он видит каждое сообщение раньше вашего обработчика.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.