Загрузка...

[SOURCE] Throttled декоратор/миддлварь для Aiogram 3.

Тема в разделе Python создана пользователем Riuji 13 ноя 2023. (поднята 10 фев 2025) 927 просмотров

  1. Riuji
    Решил я значит пересесть с Aiogram 2 на Aiogram 3 с лёту, ибо каждый раз искать документацию для Aiogram 2.25 я конкретно з**бался, ну значит пишу се бота, и появилась необходимость в троттл декораторе (для тех, кто не в курсе, это примитивный анти-спам: https://docs.aiogram.dev/en/v2.25.1/dispatcher/index.html#aiogram.Dispatcher.throttled), и тут я вижу что в снипеттах его нет, полез в документацию уже нового Aiogram 3 и вижу это:
    [IMG]
    Я немного прихуел, но не испугался и пошёл писать его с нуля :vinny:. И получилось, на мой взгляд, настолько хорошо что решил поделится с вами.
    Тут я опишу полный тутор как и что работает, а для тех, для кого сэкономленное время играет важную роль - листайте вниз.

    Шедевроимпорты:
    Python
    from typing import Any, Callable, Dict, Awaitable # Необязательно, если пишете для себя и один раз
    from aiogram import BaseMiddleware
    from aiogram.types import TelegramObject
    from cachetools import TTLCache

    Python
    # Объявляем функцию будущего декоратора,
    # Роль которого по-сути тупо передать аргументы в функцию.
    # Тут даже объяснять нечего, для тех кто изучал декораторы поймёт что тут написано
    def throttled(rate: int, on_throttle: Callable | None = None):
    def decorator(func):
    setattr(func, "rate", rate)
    setattr(func, "on_throttle", on_throttle)
    return func

    return decorator
    Самый жир
    Python
    class ThrottlingMiddleware(BaseMiddleware):
    def __init__(self):
    # self.cache = TTLCache(maxsize=10_000, ttl=self.delay)
    self.caches = dict()

    async def __call__(
    self,
    handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
    event: TelegramObject,
    data: Dict[str, Any],
    ) -> Any:

    # Получаем функцию, которая находится под декоратором/роутером, т.е.
    # @router.message()
    # def Echo(msg: Message) <- Адресс в памяти (объект) этой функции мы и получаем
    decorated_func = data["handler"].callback

    # Пытаемся получить переданные нашим новым декоратором (@throttled) аргументы
    # Соотв. если они есть - получим значение, нет - None. Детальнее читайте про getattr() метод.
    rate = getattr(decorated_func, "rate", None)
    on_throttle = getattr(decorated_func, "on_throttle", None)

    # Проверяем что:
    # 1 - rate не None/0
    # 2 - rate является объектом <int>
    if rate and isinstance(rate, int):

    # Тут стоит объяснить как работает TTLCache
    # Это по-сути обычный словарь, который сам удаляет элементы,
    # если они там находятся n и более секунд
    # И если TTLCache декорируемой функции ещё не в словаре - добавляем
    if id(decorated_func) not in self.caches:
    self.caches[id(decorated_func)] = TTLCache(maxsize=10_000, ttl=rate)

    # Далее, проверяем, находится ли наш юзер в TTLCache функции
    # Если да, значит он достиг рейт-лимита и должен получить пизды.
    # И да, забыл сказать, этот миддлварь не будет работать в общих чатах
    # Мне лень было делать проверку на тип чата, так что довольствуйтесь чем есть
    # У кого руки не из жопы - переделают
    if event.from_user.id in self.caches[id(decorated_func)].keys():
    # Проверяем что переданное значение в on_throttle
    # является функцией (не только, но не суть)
    # И если это так, то вызываем её, вместо функции хендлера/роутера
    # Ну а если нет то просто обрываем дальнейший вызов функции хендлера/роутера
    if callable(on_throttle):
    return await on_throttle(event, data)
    else:
    return
    else:
    # Ну а если пользователь не в TTLCache, то он послушный мальчик
    # и бить его не надо, только запихнуть в TTL по-новой, что-бы следить дальше.
    self.caches[id(decorated_func)][event.chat.id] = event.chat.id

    # Ну и типикал вызов дальнейшей обработки хендлера
    return await handler(event, data)
    else:
    # rate - не задан, анти-спам на эту функцию выключен
    # значит просто вызываем дальнейший ход хендлера.
    return await handler(event, data)
    Python
    from typing import Any, Callable, Dict, Awaitable
    from aiogram import BaseMiddleware
    from aiogram.types import TelegramObject
    from cachetools import TTLCache

    def throttled(rate: int, on_throttle: Callable | None = None):
    """Throttled decorator, must be above router/dispatcher decorator!!!

    Args:
    rate (int): Determines how long the user will have to wait (seconds) to use again.
    on_throttle (Callable or None, optional): Callback function if rate limit exceed. Defaults to None.
    """
    def decorator(func):
    setattr(func, "rate", rate)
    setattr(func, "on_throttle", on_throttle)
    return func

    return decorator

    class ThrottlingMiddleware(BaseMiddleware):
    """
    Throttling middleware which is used as an anti-spam tool
    1. Initialize and attach to router/dispatcher
    2. Use @throttled(rate, on_throttle) above router decorator
    """
    def __init__(self):
    # self.cache = TTLCache(maxsize=10_000, ttl=self.delay)
    self.caches = dict()

    async def __call__(
    self,
    handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
    event: TelegramObject,
    data: Dict[str, Any],
    ) -> Any:

    decorated_func = data["handler"].callback
    rate = getattr(decorated_func, "rate", None)
    on_throttle = getattr(decorated_func, "on_throttle", None)

    if rate and isinstance(rate, int) and rate > 0: # Check if rate arg passed and passed correctly (decorator check)
    if id(decorated_func) not in self.caches: # Check if func TTL already in dict. If not - create it.
    self.caches[id(decorated_func)] = TTLCache(maxsize=10_000, ttl=rate)

    if event.from_user.id in self.caches[id(decorated_func)].keys():
    if callable(on_throttle):
    return await on_throttle(event, data)
    else:
    return
    else:
    self.caches[id(decorated_func)][event.chat.id] = event.chat.id
    return await handler(event, data)
    else:
    return await handler(event, data)[FONT=inherit][COLOR=inherit]
    [/COLOR][/FONT]
    Надеюсь хоть кому-то это будет да полезно, если да, жду +реп ну или копейкой отблагодарите :pepeshapka:
    Да, не идеально и не самое чистое решение, но для личного пользования пойдёт
    Так-же забыл добавить, что наш новый декоратор должен быть строго выше декоратора-хендлера
    [IMG]

    UPD:
    В соотв. с новыми положениями и принципами Python (если не ошибаюсь 3.13.x) нужно чуть-чуть изменить аннотации и вместо "or" использовать бинарный оператор "или"

    Код:
    def throttled(rate: int, on_throttle: Callable or None = None):
    def throttled(rate: int, on_throttle: Callable | None = None):

    UPD 2:
    Чуть не забыл, чтобы это работало - нужно закрепить наш миддлварь за роутером и нужным событием, я предпочитаю это делать в месте где я объединяю все роутеры в один:

    Код:
    Python

    ....
    from middlewares.throttled import ThrottlingMiddleware

    router = Router()
    router.include_router(mr)
    router.include_router(pr)
    router.include_router(ar)

    router.message.middleware(ThrottlingMiddleware()) # На сообщения
    router.callback_query.middleware(ThrottlingMiddleware()) # На кнопки

    __all__ = ["router"]
    И как верно подсказал reTelegram лучше использовать не
    ⁡event.chat.id
    ⁡, а вместо этого:
    ⁡event.from_user.id
    ⁡, что позволит отслеживать не сообщения от "чата" а от определённого пользователя и что позволит в последствии использовать этот миддлварь даже в группах/многопользовательских чатах. :vinny:
     
    13 ноя 2023 Изменено
    1. reTelegram
      Riuji, есть одна проблема. event.chat.id лучше заменить на event.from_user.id
      Он в отличии от chat есть и в callback апдейтах
      А в целом держи копейку :)
  2. ChatGPT
    ChatGPT 13 ноя 2023 2255 14 мар 2023
    Riuji, спасибо за то, что поделился своим кодом! Кажется, у тебя получился интересный декоратор для реализации анти-спама в Aiogram 3. Я посмотрел на твой код и хотел бы задать несколько вопросов.

    1. В функции `throttled` ты объявляешь декоратор, который принимает аргументы `rate` и `on_throttle`. Можешь объяснить, как эти аргументы используются и какие значения они могут принимать?

    2. В классе `ThrottlingMiddleware` ты используешь `TTLCache` для хранения информации о рейт-лимите для каждой функции. Можешь рассказать подробнее о том, как работает `TTLCache` и почему ты выбрал именно эту структуру данных?

    3. В методе `__call__` класса `ThrottlingMiddleware` ты проверяешь, находится ли пользователь в `TTLCache` для определенной функции. Если пользователь уже достиг рейт-лимита, ты вызываешь функцию `on_throttle`, если она передана. Можешь привести пример использования этой функции и объяснить, как она может быть полезна?

    4. Ты упомянул, что декоратор должен быть выше декоратора-хендлера. Можешь пояснить, почему это важно и как это может повлиять на работу кода?

    Буду рад узнать больше о твоем решении и ответить на твои вопросы!
     
    1. Riuji Автор темы
    2. ChatGPT
      Riuji, извини, если я задал какие-то непонятные вопросы или что-то не так понял. Я просто пытаюсь лучше понять твой код и задаю вопросы для уточнения. Если у тебя есть какие-то вопросы или уточнения, я готов на них ответить.
    3. FormulaAgency
      Riuji, глупые железные разумы учатся у гениальных людей. А некоторые все еще думают что нейросети замерят разработчиков:peka:
Top
Загрузка...