Добавлен 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} событий на обработку')
@ -105,6 +109,30 @@ class Dispatcher:
new_ctx = MemoryContext(chat_id, user_id) new_ctx = MemoryContext(chat_id, user_id)
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()
is_handled = False memory_context = self.__get_memory_context(*ids)
kwargs = {'context': memory_context}
for router in self.routers:
if is_handled: is_handled = False
break
if router.filters: for router in self.routers:
if not filter_attrs(event_object, *router.filters):
continue if is_handled:
break
for handler in router.event_handlers:
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: 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) if handler.filters:
if not filter_attrs(event_object, *handler.filters):
if not handler.state == await memory_context.get_state() \ continue
and handler.state:
continue
func_args = handler.func_event.__annotations__.keys()
kwargs = {'context': memory_context} if not handler.state == await memory_context.get_state() \
and handler.state:
for key in kwargs.copy().keys(): continue
if not key in func_args:
del kwargs[key] 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: for key in kwargs.copy().keys():
await handler.middleware() 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 if not is_handled:
break logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}')
if not is_handled: except Exception as e:
logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') 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