Добавлен Middleware

This commit is contained in:
Денис Семёнов 2025-06-23 09:49:24 +03:00
parent 8aa9c65fcc
commit 512eb9a4af
9 changed files with 253 additions and 49 deletions

View File

@ -66,12 +66,16 @@ if __name__ == '__main__':
- [Обработчик доступных событий](https://github.com/love-apples/maxapi/blob/main/examples/events/main.py) - [Обработчик доступных событий](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) - [Обработчики с 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) - [Демонстрация роутинга, 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
- ✅ Роутеры - ✅ Роутеры
- ✅ Билдер инлайн клавиатур - ✅ Билдер инлайн клавиатур
- ✅ Простая загрузка медиафайлов - ✅ Простая загрузка медиафайлов

39
examples/get_ids/main.py Normal file
View File

@ -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'Из чата: <b>{event.message.link.chat_id}</b>\n'
f'От пользователя: <b>{event.message.link.sender.user_id}</b>'
)
await event.message.reply(text)
@dp.message_created()
async def get_ids(event: MessageCreated):
text = (
f'Ваш ID: <b>{event.from_user.user_id}</b>\n'
f'ID этого чата: <b>{event.chat.chat_id}</b>'
)
await event.message.answer(text, parse_mode=ParseMode.HTML)
async def main():
await dp.start_polling(bot)
if __name__ == '__main__':
asyncio.run(main())

View File

@ -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())

View File

@ -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())

View File

@ -1,4 +1,4 @@
from typing import Callable, List from typing import Any, Callable, Dict, List
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@ -6,6 +6,8 @@ from magic_filter import MagicFilter
from uvicorn import Config, Server from uvicorn import Config, Server
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
from maxapi.filters.middleware import BaseMiddleware
from .filters.handler import Handler from .filters.handler import Handler
from .context import MemoryContext from .context import MemoryContext
@ -36,6 +38,8 @@ class Dispatcher:
self.contexts: List[MemoryContext] = [] self.contexts: List[MemoryContext] = []
self.routers: List[Router] = [] self.routers: List[Router] = []
self.filters: List[MagicFilter] = [] self.filters: List[MagicFilter] = []
self.middlewares: List[BaseMiddleware] = []
self.bot = None self.bot = None
self.on_started_func = None self.on_started_func = None
@ -78,7 +82,7 @@ class Dispatcher:
handlers_count = 0 handlers_count = 0
for router in self.routers: for router in self.routers:
for handler in router.event_handlers: for _ in router.event_handlers:
handlers_count += 1 handlers_count += 1
logger_dp.info(f'{handlers_count} событий на обработку') logger_dp.info(f'{handlers_count} событий на обработку')
@ -106,6 +110,30 @@ class Dispatcher:
self.contexts.append(new_ctx) self.contexts.append(new_ctx)
return 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): async def handle(self, event_object: UpdateUnion):
"""Обрабатывает событие. """Обрабатывает событие.
@ -113,54 +141,68 @@ class Dispatcher:
Args: Args:
event_object: Объект события для обработки event_object: Объект события для обработки
""" """
ids = event_object.get_ids() try:
ids = event_object.get_ids()
memory_context = self.__get_memory_context(*ids)
kwargs = {'context': memory_context}
is_handled = False is_handled = False
for router in self.routers: for router in self.routers:
if is_handled: if is_handled:
break break
if router.filters: if router.filters:
if not filter_attrs(event_object, *router.filters): if not filter_attrs(event_object, *router.filters):
continue
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):
continue continue
memory_context = self.__get_memory_context(*ids) kwargs = await self.process_middlewares(
middlewares=router.middlewares,
event_object=event_object,
result_data_kwargs=kwargs
)
if not handler.state == await memory_context.get_state() \ for handler in router.event_handlers:
and handler.state:
continue
func_args = handler.func_event.__annotations__.keys() if not handler.update_type == event_object.update_type:
continue
kwargs = {'context': memory_context} if handler.filters:
if not filter_attrs(event_object, *handler.filters):
continue
for key in kwargs.copy().keys(): if not handler.state == await memory_context.get_state() \
if not key in func_args: and handler.state:
del kwargs[key] continue
if handler.middleware: func_args = handler.func_event.__annotations__.keys()
await handler.middleware()
await handler.func_event(event_object, **kwargs) kwargs = await self.process_middlewares(
middlewares=handler.middlewares,
event_object=event_object,
result_data_kwargs=kwargs
)
logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') if not kwargs:
continue
is_handled = True for key in kwargs.copy().keys():
break if not key in func_args:
del kwargs[key]
if not is_handled: 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
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): async def start_polling(self, bot: Bot):
@ -187,10 +229,7 @@ class Dispatcher:
) )
for event in processed_events: for event in processed_events:
try: await self.handle(event)
await self.handle(event)
except Exception as e:
logger_dp.error(f"Ошибка при обработке события: {event.update_type}: {e}")
except ClientConnectorError: except ClientConnectorError:
logger_dp.error(f'Ошибка подключения: {e}') logger_dp.error(f'Ошибка подключения: {e}')
except Exception as e: except Exception as e:

View File

@ -1,4 +1,4 @@
from typing import Callable from typing import Callable, List
from magic_filter import F, MagicFilter from magic_filter import F, MagicFilter
@ -45,7 +45,7 @@ class Handler:
self.update_type: UpdateType = update_type self.update_type: UpdateType = update_type
self.filters = [] self.filters = []
self.state: State = None self.state: State = None
self.middleware: BaseMiddleware = None self.middlewares: List[BaseMiddleware] = []
for arg in args: for arg in args:
if isinstance(arg, MagicFilter): if isinstance(arg, MagicFilter):
@ -55,7 +55,7 @@ class Handler:
elif isinstance(arg, Command): elif isinstance(arg, Command):
self.filters.insert(0, F.message.body.text.startswith(arg.command)) self.filters.insert(0, F.message.body.text.startswith(arg.command))
elif isinstance(arg, BaseMiddleware): elif isinstance(arg, BaseMiddleware):
self.middleware = arg self.middlewares.append(arg)
else: else:
logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при ' logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при '
f'регистрации функции `{func_event.__name__}`') f'регистрации функции `{func_event.__name__}`')

View File

@ -1,6 +1,23 @@
from typing import Any, Dict
from ..types.updates import UpdateUnion from ..types.updates import UpdateUnion
class BaseMiddleware: class BaseMiddleware:
def __init__(self): def __init__(self):
... ...
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

View File

@ -9,6 +9,7 @@ from ..types.updates.message_edited import MessageEdited
from ..types.updates.message_removed import MessageRemoved from ..types.updates.message_removed import MessageRemoved
from ..types.updates.user_added import UserAdded from ..types.updates.user_added import UserAdded
from ..types.updates.user_removed import UserRemoved from ..types.updates.user_removed import UserRemoved
from ..types.updates import UpdateUnion
from ..types.attachments.attachment import PhotoAttachmentPayload from ..types.attachments.attachment import PhotoAttachmentPayload
from ..types.attachments.attachment import OtherAttachmentPayload 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.link_button import LinkButton
from ..types.attachments.buttons.request_contact import RequestContact from ..types.attachments.buttons.request_contact import RequestContact
from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton
from ..types.message import Message
from ..types.command import Command, BotCommand from ..types.command import Command, BotCommand
from .input_media import InputMedia from .input_media import InputMedia
__all__ = [ __all__ = [
UpdateUnion,
InputMedia, InputMedia,
BotCommand, BotCommand,
CallbackButton, CallbackButton,

View File

@ -1,5 +1,7 @@
from typing import Optional from typing import Optional
from maxapi.enums.button_type import ButtonType
from .button import Button from .button import Button
@ -7,7 +9,6 @@ class ChatButton(Button):
""" """
Attributes: Attributes:
type: Тип кнопки (наследуется от Button)
text: Текст кнопки (наследуется от Button) text: Текст кнопки (наследуется от Button)
chat_title: Название чата (до 128 символов) chat_title: Название чата (до 128 символов)
chat_description: Описание чата (до 256 символов) chat_description: Описание чата (до 256 символов)
@ -15,6 +16,7 @@ class ChatButton(Button):
uuid: Уникальный идентификатор чата uuid: Уникальный идентификатор чата
""" """
type: ButtonType = ButtonType.CHAT
chat_title: Optional[str] = None chat_title: Optional[str] = None
chat_description: Optional[str] = None chat_description: Optional[str] = None
start_payload: Optional[str] = None start_payload: Optional[str] = None