From 512eb9a4af230fcf9a21da214a9d18130d8bd370 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 23 Jun 2025 09:49:24 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20Middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 + examples/get_ids/main.py | 39 ++++++ examples/middleware_for_router/main.py | 41 ++++++ examples/middleware_in_handlers/main.py | 59 ++++++++ maxapi/dispatcher.py | 127 ++++++++++++------ maxapi/filters/handler.py | 6 +- maxapi/filters/middleware.py | 19 ++- maxapi/types/__init__.py | 3 + .../types/attachments/buttons/chat_button.py | 4 +- 9 files changed, 253 insertions(+), 49 deletions(-) create mode 100644 examples/get_ids/main.py create mode 100644 examples/middleware_for_router/main.py create mode 100644 examples/middleware_in_handlers/main.py diff --git a/README.md b/README.md index b82af95..fffc3c2 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,16 @@ if __name__ == '__main__': - [Обработчик доступных событий](https://github.com/love-apples/maxapi/blob/main/examples/events/main.py) - [Обработчики с MagicFilter](https://github.com/love-apples/maxapi/blob/main/examples/magic_filters/main.py) - [Демонстрация роутинга, InputMedia и механика контекста](https://github.com/love-apples/maxapi/tree/main/examples/router_with_input_media) (audio.mp3 для команды /media) + - [Получение ID](https://github.com/love-apples/maxapi/tree/main/examples/get_ids/main.py) + - [Миддлварь в хендлерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_in_handlers/main.py) + - [Миддлварь в роутерах](https://github.com/love-apples/maxapi/tree/main/examples/middleware_for_router/main.py) --- ## 🧩 Возможности +- ✅ Middleware - ✅ Роутеры - ✅ Билдер инлайн клавиатур - ✅ Простая загрузка медиафайлов diff --git a/examples/get_ids/main.py b/examples/get_ids/main.py new file mode 100644 index 0000000..df26e8a --- /dev/null +++ b/examples/get_ids/main.py @@ -0,0 +1,39 @@ +import asyncio +import logging + +from maxapi import Bot, Dispatcher, F +from maxapi.enums.parse_mode import ParseMode +from maxapi.types import MessageCreated + +logging.basicConfig(level=logging.INFO) + +bot = Bot('тут_ваш_токен') +dp = Dispatcher() + + +@dp.message_created(F.message.link.type == 'forward') +async def get_ids_from_forward(event: MessageCreated): + text = ( + 'Информация о пересланном сообщении:\n\n' + + f'Из чата: {event.message.link.chat_id}\n' + f'От пользователя: {event.message.link.sender.user_id}' + ) + await event.message.reply(text) + + +@dp.message_created() +async def get_ids(event: MessageCreated): + text = ( + f'Ваш ID: {event.from_user.user_id}\n' + f'ID этого чата: {event.chat.chat_id}' + ) + await event.message.answer(text, parse_mode=ParseMode.HTML) + + +async def main(): + await dp.start_polling(bot) + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/examples/middleware_for_router/main.py b/examples/middleware_for_router/main.py new file mode 100644 index 0000000..730d18c --- /dev/null +++ b/examples/middleware_for_router/main.py @@ -0,0 +1,41 @@ +import asyncio +import logging + +from typing import Any, Dict + +from maxapi import Bot, Dispatcher +from maxapi.types import MessageCreated, Command, UpdateUnion +from maxapi.filters.middleware import BaseMiddleware + +logging.basicConfig(level=logging.INFO) + +bot = Bot(token='тут_ваш_токен') +dp = Dispatcher() + + +class CustomDataForRouterMiddleware(BaseMiddleware): + async def __call__( + self, + event: UpdateUnion, + data: Dict[str, Any] + ): + + data['custom_data'] = f'Это ID того кто вызвал команду: {event.from_user.user_id}' + + return data + + +@dp.message_created(Command('custom_data')) +async def custom_data(event: MessageCreated, custom_data: str): + await event.message.answer(custom_data) + + +async def main(): + dp.middlewares = [ + CustomDataForRouterMiddleware() + ] + await dp.start_polling(bot) + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/examples/middleware_in_handlers/main.py b/examples/middleware_in_handlers/main.py new file mode 100644 index 0000000..3a3c3e2 --- /dev/null +++ b/examples/middleware_in_handlers/main.py @@ -0,0 +1,59 @@ +import asyncio +import logging + +from typing import Any, Dict + +from maxapi import Bot, Dispatcher +from maxapi.filters.middleware import BaseMiddleware +from maxapi.types import MessageCreated, Command, UpdateUnion +from maxapi.types.command import Command + +logging.basicConfig(level=logging.INFO) + +bot = Bot(token='тут_ваш_токен') +dp = Dispatcher() + + +class CheckChatTitleMiddleware(BaseMiddleware): + async def __call__( + self, + event: UpdateUnion, + ): + + return event.chat.title == 'MAXApi' + + +@dp.message_created(Command('start'), CheckChatTitleMiddleware()) +async def start(event: MessageCreated): + await event.message.answer('Это сообщение было отправлено, так как ваш чат называется "MAXApi"!') + + +class CustomDataMiddleware(BaseMiddleware): + async def __call__( + self, + event: UpdateUnion, + data: Dict[str, Any] + ): + + data['custom_data'] = f'Это ID того кто вызвал команду: {event.from_user.user_id}' + + return data + + +@dp.message_created(Command('custom_data'), CustomDataMiddleware()) +async def custom_data(event: MessageCreated, custom_data: str): + await event.message.answer(custom_data) + + +@dp.message_created(Command('many_middlewares'), CheckChatTitleMiddleware(), CustomDataMiddleware()) +async def many_middlewares(event: MessageCreated, custom_data: str): + await event.message.answer('Это сообщение было отправлено, так как ваш чат называется "MAXApi"!') + await event.message.answer(custom_data) + + +async def main(): + await dp.start_polling(bot) + + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/maxapi/dispatcher.py b/maxapi/dispatcher.py index b1ab861..4b5d83a 100644 --- a/maxapi/dispatcher.py +++ b/maxapi/dispatcher.py @@ -1,4 +1,4 @@ -from typing import Callable, List +from typing import Any, Callable, Dict, List from fastapi import FastAPI, Request from fastapi.responses import JSONResponse @@ -6,6 +6,8 @@ from magic_filter import MagicFilter from uvicorn import Config, Server from aiohttp import ClientConnectorError +from maxapi.filters.middleware import BaseMiddleware + from .filters.handler import Handler from .context import MemoryContext @@ -36,6 +38,8 @@ class Dispatcher: self.contexts: List[MemoryContext] = [] self.routers: List[Router] = [] self.filters: List[MagicFilter] = [] + self.middlewares: List[BaseMiddleware] = [] + self.bot = None self.on_started_func = None @@ -78,7 +82,7 @@ class Dispatcher: handlers_count = 0 for router in self.routers: - for handler in router.event_handlers: + for _ in router.event_handlers: handlers_count += 1 logger_dp.info(f'{handlers_count} событий на обработку') @@ -105,6 +109,30 @@ class Dispatcher: new_ctx = MemoryContext(chat_id, user_id) self.contexts.append(new_ctx) return new_ctx + + async def process_middlewares( + self, + middlewares: List[BaseMiddleware], + event_object: UpdateUnion, + result_data_kwargs: Dict[str, Any] + ): + + for middleware in middlewares: + result = await middleware.process_middleware( + event_object=event_object, + result_data_kwargs=result_data_kwargs + ) + + if result == None or result == False: + return + + elif result == True: + result = {} + + for key, value in result.items(): + result_data_kwargs[key] = value + + return result_data_kwargs async def handle(self, event_object: UpdateUnion): @@ -113,54 +141,68 @@ class Dispatcher: Args: event_object: Объект события для обработки """ - ids = event_object.get_ids() - - is_handled = False - - for router in self.routers: + try: + ids = event_object.get_ids() + memory_context = self.__get_memory_context(*ids) + kwargs = {'context': memory_context} - if is_handled: - break + is_handled = False - if router.filters: - if not filter_attrs(event_object, *router.filters): - continue - - for handler in router.event_handlers: + for router in self.routers: + + if is_handled: + break + + if router.filters: + if not filter_attrs(event_object, *router.filters): + continue + + kwargs = await self.process_middlewares( + middlewares=router.middlewares, + event_object=event_object, + result_data_kwargs=kwargs + ) + + for handler in router.event_handlers: - if not handler.update_type == event_object.update_type: - continue - - if handler.filters: - if not filter_attrs(event_object, *handler.filters): + if not handler.update_type == event_object.update_type: continue - memory_context = self.__get_memory_context(*ids) - - if not handler.state == await memory_context.get_state() \ - and handler.state: - continue - - func_args = handler.func_event.__annotations__.keys() + if handler.filters: + if not filter_attrs(event_object, *handler.filters): + continue - kwargs = {'context': memory_context} - - for key in kwargs.copy().keys(): - if not key in func_args: - del kwargs[key] + if not handler.state == await memory_context.get_state() \ + and handler.state: + continue + + func_args = handler.func_event.__annotations__.keys() + + kwargs = await self.process_middlewares( + middlewares=handler.middlewares, + event_object=event_object, + result_data_kwargs=kwargs + ) + + if not kwargs: + continue - if handler.middleware: - await handler.middleware() + for key in kwargs.copy().keys(): + if not key in func_args: + del kwargs[key] + + await handler.func_event(event_object, **kwargs) - await handler.func_event(event_object, **kwargs) + logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') - logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') + is_handled = True + break - is_handled = True - break - - if not is_handled: - logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') + if not is_handled: + logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') + + except Exception as e: + logger_dp.error(f"Ошибка при обработке события: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]} | {e} ") async def start_polling(self, bot: Bot): @@ -187,10 +229,7 @@ class Dispatcher: ) for event in processed_events: - try: - await self.handle(event) - except Exception as e: - logger_dp.error(f"Ошибка при обработке события: {event.update_type}: {e}") + await self.handle(event) except ClientConnectorError: logger_dp.error(f'Ошибка подключения: {e}') except Exception as e: diff --git a/maxapi/filters/handler.py b/maxapi/filters/handler.py index 16f246d..72bdfd4 100644 --- a/maxapi/filters/handler.py +++ b/maxapi/filters/handler.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, List from magic_filter import F, MagicFilter @@ -45,7 +45,7 @@ class Handler: self.update_type: UpdateType = update_type self.filters = [] self.state: State = None - self.middleware: BaseMiddleware = None + self.middlewares: List[BaseMiddleware] = [] for arg in args: if isinstance(arg, MagicFilter): @@ -55,7 +55,7 @@ class Handler: elif isinstance(arg, Command): self.filters.insert(0, F.message.body.text.startswith(arg.command)) elif isinstance(arg, BaseMiddleware): - self.middleware = arg + self.middlewares.append(arg) else: logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при ' f'регистрации функции `{func_event.__name__}`') \ No newline at end of file diff --git a/maxapi/filters/middleware.py b/maxapi/filters/middleware.py index a50196f..2da7d6a 100644 --- a/maxapi/filters/middleware.py +++ b/maxapi/filters/middleware.py @@ -1,6 +1,23 @@ +from typing import Any, Dict from ..types.updates import UpdateUnion class BaseMiddleware: def __init__(self): - ... \ No newline at end of file + ... + + async def process_middleware( + self, + result_data_kwargs: Dict[str, Any], + event_object: UpdateUnion + ): + + kwargs_temp = {'data': result_data_kwargs.copy()} + + for key in kwargs_temp.copy().keys(): + if not key in self.__call__.__annotations__.keys(): + del kwargs_temp[key] + + result: Dict[str, Any] = await self(event_object, **kwargs_temp) + + return result \ No newline at end of file diff --git a/maxapi/types/__init__.py b/maxapi/types/__init__.py index 8feacfe..b6e4de7 100644 --- a/maxapi/types/__init__.py +++ b/maxapi/types/__init__.py @@ -9,6 +9,7 @@ from ..types.updates.message_edited import MessageEdited from ..types.updates.message_removed import MessageRemoved from ..types.updates.user_added import UserAdded from ..types.updates.user_removed import UserRemoved +from ..types.updates import UpdateUnion from ..types.attachments.attachment import PhotoAttachmentPayload from ..types.attachments.attachment import OtherAttachmentPayload @@ -20,12 +21,14 @@ from ..types.attachments.buttons.chat_button import ChatButton from ..types.attachments.buttons.link_button import LinkButton from ..types.attachments.buttons.request_contact import RequestContact from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton +from ..types.message import Message from ..types.command import Command, BotCommand from .input_media import InputMedia __all__ = [ + UpdateUnion, InputMedia, BotCommand, CallbackButton, diff --git a/maxapi/types/attachments/buttons/chat_button.py b/maxapi/types/attachments/buttons/chat_button.py index 4f40610..b012515 100644 --- a/maxapi/types/attachments/buttons/chat_button.py +++ b/maxapi/types/attachments/buttons/chat_button.py @@ -1,5 +1,7 @@ from typing import Optional +from maxapi.enums.button_type import ButtonType + from .button import Button @@ -7,7 +9,6 @@ class ChatButton(Button): """ Attributes: - type: Тип кнопки (наследуется от Button) text: Текст кнопки (наследуется от Button) chat_title: Название чата (до 128 символов) chat_description: Описание чата (до 256 символов) @@ -15,6 +16,7 @@ class ChatButton(Button): uuid: Уникальный идентификатор чата """ + type: ButtonType = ButtonType.CHAT chat_title: Optional[str] = None chat_description: Optional[str] = None start_payload: Optional[str] = None