Compare commits

...

10 Commits

21 changed files with 488 additions and 175 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
test.py
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class

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,69 +1,69 @@
from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, TYPE_CHECKING from typing import Any, Dict, List, Optional, TYPE_CHECKING
from maxapi.methods.download_media import DownloadMedia
from .methods.get_upload_url import GetUploadURL
from .methods.get_updates import GetUpdates
from .methods.remove_member_chat import RemoveMemberChat
from .methods.add_admin_chat import AddAdminChat
from .methods.add_members_chat import AddMembersChat
from .methods.get_members_chat import GetMembersChat
from .methods.remove_admin import RemoveAdmin
from .methods.get_list_admin_chat import GetListAdminChat
from .methods.delete_bot_from_chat import DeleteMeFromMessage
from .methods.get_me_from_chat import GetMeFromChat
from .methods.delete_pin_message import DeletePinMessage
from .methods.get_pinned_message import GetPinnedMessage
from .methods.pin_message import PinMessage
from .methods.delete_chat import DeleteChat
from .methods.send_action import SendAction
from .methods.edit_chat import EditChat
from .methods.get_chat_by_id import GetChatById
from .methods.get_chat_by_link import GetChatByLink
from .methods.send_callback import SendCallback
from .methods.get_video import GetVideo
from .methods.delete_message import DeleteMessage
from .methods.edit_message import EditMessage
from .methods.change_info import ChangeInfo
from .methods.get_me import GetMe
from .methods.get_messages import GetMessages
from .methods.get_chats import GetChats
from .methods.send_message import SendMessage
from .connection.base import BaseConnection
from .enums.parse_mode import ParseMode from .enums.parse_mode import ParseMode
from .enums.sender_action import SenderAction from .enums.sender_action import SenderAction
from .enums.upload_type import UploadType from .enums.upload_type import UploadType
from .types.message import Message from .methods.add_admin_chat import AddAdminChat
from .types.attachments.attachment import Attachment from .methods.add_members_chat import AddMembersChat
from .types.attachments.image import PhotoAttachmentRequestPayload from .methods.change_info import ChangeInfo
from .types.message import Messages, NewMessageLink from .methods.delete_bot_from_chat import DeleteMeFromMessage
from .types.users import ChatAdmin, User from .methods.delete_chat import DeleteChat
from .types.command import BotCommand from .methods.delete_message import DeleteMessage
from .methods.delete_pin_message import DeletePinMessage
from .methods.download_media import DownloadMedia
from .methods.edit_chat import EditChat
from .methods.edit_message import EditMessage
from .methods.get_chat_by_id import GetChatById
from .methods.get_chat_by_link import GetChatByLink
from .methods.get_chats import GetChats
from .methods.get_list_admin_chat import GetListAdminChat
from .methods.get_me import GetMe
from .methods.get_me_from_chat import GetMeFromChat
from .methods.get_members_chat import GetMembersChat
from .methods.get_messages import GetMessages
from .methods.get_pinned_message import GetPinnedMessage
from .methods.get_updates import GetUpdates
from .methods.get_upload_url import GetUploadURL
from .methods.get_video import GetVideo
from .methods.pin_message import PinMessage
from .methods.remove_admin import RemoveAdmin
from .methods.remove_member_chat import RemoveMemberChat
from .methods.send_action import SendAction
from .methods.send_callback import SendCallback
from .methods.send_message import SendMessage
from .connection.base import BaseConnection if TYPE_CHECKING:
from .types.attachments.attachment import Attachment
from .types.attachments.image import PhotoAttachmentRequestPayload
from .types.attachments.video import Video
from .types.chats import Chat, ChatMember, Chats
from .types.command import BotCommand
from .types.message import Message, Messages, NewMessageLink
from .types.updates import UpdateUnion
from .types.users import ChatAdmin, User
from .methods.types.added_admin_chat import AddedListAdminChat from .methods.types.added_admin_chat import AddedListAdminChat
from .methods.types.added_members_chat import AddedMembersChat from .methods.types.added_members_chat import AddedMembersChat
from .methods.types.deleted_bot_from_chat import DeletedBotFromChat from .methods.types.deleted_bot_from_chat import DeletedBotFromChat
from .methods.types.deleted_chat import DeletedChat from .methods.types.deleted_chat import DeletedChat
from .methods.types.deleted_message import DeletedMessage from .methods.types.deleted_message import DeletedMessage
from .methods.types.deleted_pin_message import DeletedPinMessage from .methods.types.deleted_pin_message import DeletedPinMessage
from .methods.types.edited_message import EditedMessage from .methods.types.edited_message import EditedMessage
from .methods.types.getted_list_admin_chat import GettedListAdminChat from .methods.types.getted_list_admin_chat import GettedListAdminChat
from .methods.types.getted_members_chat import GettedMembersChat from .methods.types.getted_members_chat import GettedMembersChat
from .methods.types.getted_pineed_message import GettedPin from .methods.types.getted_pineed_message import GettedPin
from .methods.types.getted_upload_url import GettedUploadUrl from .methods.types.getted_upload_url import GettedUploadUrl
from .methods.types.pinned_message import PinnedMessage from .methods.types.pinned_message import PinnedMessage
from .methods.types.removed_admin import RemovedAdmin from .methods.types.removed_admin import RemovedAdmin
from .methods.types.removed_member_chat import RemovedMemberChat from .methods.types.removed_member_chat import RemovedMemberChat
from .methods.types.sended_action import SendedAction from .methods.types.sended_action import SendedAction
from .methods.types.sended_callback import SendedCallback from .methods.types.sended_callback import SendedCallback
from .methods.types.sended_message import SendedMessage from .methods.types.sended_message import SendedMessage
from .types.attachments.video import Video
from .types.chats import Chat, ChatMember, Chats
from .types.updates import UpdateUnion
class Bot(BaseConnection): class Bot(BaseConnection):
@ -74,11 +74,21 @@ class Bot(BaseConnection):
пользователями и другими функциями бота. пользователями и другими функциями бота.
""" """
def __init__(self, token: str): def __init__(
self,
token: str,
parse_mode: Optional[ParseMode] = None,
notify: Optional[bool] = None,
auto_requests: bool = True,
):
"""Инициализирует экземпляр бота с указанным токеном. """Инициализирует экземпляр бота с указанным токеном.
:param token: Токен доступа к API бота :param token: Токен доступа к API бота
:param parse_mode: Форматирование по умолчанию
:param notify: Отключение уведомлений при отправке сообщений (по умолчанию игнорируется) (не работает на стороне MAX)
:param auto_requests: Автоматическое заполнение полей chat и from_user в Update
с помощью API запросов если они не заложены как полноценные объекты в Update (по умолчанию True, при False chat и from_user в некоторых событиях будут выдавать None)
""" """
super().__init__() super().__init__()
@ -89,27 +99,35 @@ class Bot(BaseConnection):
self.params = {'access_token': self.__token} self.params = {'access_token': self.__token}
self.marker_updates = None self.marker_updates = None
self.parse_mode = parse_mode
self.notify = notify
self.auto_requests = auto_requests
def _resolve_notify(self, notify: Optional[bool]) -> Optional[bool]:
return notify if notify is not None else self.notify
def _resolve_parse_mode(self, mode: Optional[ParseMode]) -> Optional[ParseMode]:
return mode if mode is not None else self.parse_mode
async def send_message( async def send_message(
self, self,
chat_id: int = None, chat_id: int = None,
user_id: int = None, user_id: int = None,
disable_link_preview: bool = False,
text: str = None, text: str = None,
attachments: List[Attachment] = None, attachments: List[Attachment] = None,
link: NewMessageLink = None, link: NewMessageLink = None,
notify: bool = True, notify: Optional[bool] = None,
parse_mode: ParseMode = None parse_mode: Optional[ParseMode] = None
) -> SendedMessage: ) -> SendedMessage:
"""Отправляет сообщение в чат или пользователю. """Отправляет сообщение в чат или пользователю.
:param chat_id: ID чата для отправки (обязателен, если не указан user_id) :param chat_id: ID чата для отправки (обязателен, если не указан user_id)
:param user_id: ID пользователя для отправки (обязателен, если не указан chat_id) :param user_id: ID пользователя для отправки (обязателен, если не указан chat_id)
:param disable_link_preview: Отключить предпросмотр ссылок (по умолчанию False)
:param text: Текст сообщения :param text: Текст сообщения
:param attachments: Список вложений к сообщению :param attachments: Список вложений к сообщению
:param link: Данные ссылки сообщения :param link: Данные ссылки сообщения
:param notify: Отправлять уведомление получателю (по умолчанию True) :param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:param parse_mode: Режим форматирования текста :param parse_mode: Режим форматирования текста
:return: Объект отправленного сообщения :return: Объект отправленного сообщения
@ -119,12 +137,11 @@ class Bot(BaseConnection):
bot=self, bot=self,
chat_id=chat_id, chat_id=chat_id,
user_id=user_id, user_id=user_id,
disable_link_preview=disable_link_preview,
text=text, text=text,
attachments=attachments, attachments=attachments,
link=link, link=link,
notify=notify, notify=self._resolve_notify(notify),
parse_mode=parse_mode parse_mode=self._resolve_parse_mode(notify)
).request() ).request()
async def send_action( async def send_action(
@ -153,8 +170,8 @@ class Bot(BaseConnection):
text: str = None, text: str = None,
attachments: List[Attachment] = None, attachments: List[Attachment] = None,
link: NewMessageLink = None, link: NewMessageLink = None,
notify: bool = True, notify: Optional[bool] = None,
parse_mode: ParseMode = None parse_mode: Optional[ParseMode] = None
) -> EditedMessage: ) -> EditedMessage:
"""Редактирует существующее сообщение. """Редактирует существующее сообщение.
@ -163,7 +180,7 @@ class Bot(BaseConnection):
:param text: Новый текст сообщения :param text: Новый текст сообщения
:param attachments: Новые вложения :param attachments: Новые вложения
:param link: Новая ссылка сообщения :param link: Новая ссылка сообщения
:param notify: Уведомлять получателя об изменении (по умолчанию True) :param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:param parse_mode: Режим форматирования текста :param parse_mode: Режим форматирования текста
:return: Объект отредактированного сообщения :return: Объект отредактированного сообщения
@ -175,8 +192,8 @@ class Bot(BaseConnection):
text=text, text=text,
attachments=attachments, attachments=attachments,
link=link, link=link,
notify=notify, notify=self._resolve_notify(notify),
parse_mode=parse_mode parse_mode=self._resolve_parse_mode(notify)
).request() ).request()
async def delete_message( async def delete_message(
@ -364,7 +381,7 @@ class Bot(BaseConnection):
icon: PhotoAttachmentRequestPayload = None, icon: PhotoAttachmentRequestPayload = None,
title: str = None, title: str = None,
pin: str = None, pin: str = None,
notify: bool = True, notify: Optional[bool] = None,
) -> Chat: ) -> Chat:
"""Редактирует параметры чата. """Редактирует параметры чата.
@ -373,7 +390,7 @@ class Bot(BaseConnection):
:param icon: Данные иконки чата :param icon: Данные иконки чата
:param title: Новый заголовок чата :param title: Новый заголовок чата
:param pin: ID сообщения для закрепления :param pin: ID сообщения для закрепления
:param notify: Уведомлять участников (по умолчанию True) :param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:return: Обновленный объект чата :return: Обновленный объект чата
""" """
@ -384,7 +401,7 @@ class Bot(BaseConnection):
icon=icon, icon=icon,
title=title, title=title,
pin=pin, pin=pin,
notify=notify notify=self._resolve_notify(notify),
).request() ).request()
async def get_video( async def get_video(
@ -407,7 +424,7 @@ class Bot(BaseConnection):
async def send_callback( async def send_callback(
self, self,
callback_id: str, callback_id: str,
message: 'Message' = None, message: Message = None,
notification: str = None notification: str = None
) -> SendedCallback: ) -> SendedCallback:
@ -431,14 +448,14 @@ class Bot(BaseConnection):
self, self,
chat_id: int, chat_id: int,
message_id: str, message_id: str,
notify: bool = True notify: Optional[bool] = None
) -> PinnedMessage: ) -> PinnedMessage:
"""Закрепляет сообщение в чате. """Закрепляет сообщение в чате.
:param chat_id: ID чата :param chat_id: ID чата
:param message_id: ID сообщения :param message_id: ID сообщения
:param notify: Уведомлять участников (по умолчанию True) :param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:return: Закрепленное сообщение :return: Закрепленное сообщение
""" """
@ -447,7 +464,7 @@ class Bot(BaseConnection):
bot=self, bot=self,
chat_id=chat_id, chat_id=chat_id,
message_id=message_id, message_id=message_id,
notify=notify notify=self._resolve_notify(notify),
).request() ).request()
async def delete_pin_message( async def delete_pin_message(

View File

@ -113,8 +113,8 @@ class BaseConnection:
:return: Сырой .text() ответ от сервера после загрузки файла :return: Сырой .text() ответ от сервера после загрузки файла
""" """
with open(path, 'rb') as f: async with aiofiles.open(path, 'rb') as f:
file_data = f.read() file_data = await f.read()
basename = os.path.basename(path) basename = os.path.basename(path)
_, ext = os.path.splitext(basename) _, ext = os.path.splitext(basename)

View File

@ -1,4 +1,6 @@
from typing import Callable, List import asyncio
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 +8,7 @@ from magic_filter import MagicFilter
from uvicorn import Config, Server from uvicorn import Config, Server
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
from .filters.middleware import BaseMiddleware
from .filters.handler import Handler from .filters.handler import Handler
from .context import MemoryContext from .context import MemoryContext
@ -21,7 +24,8 @@ from .enums.update import UpdateType
from .loggers import logger_dp from .loggers import logger_dp
app = FastAPI() webhook_app = FastAPI()
CONNECTION_RETRY_DELAY = 30
class Dispatcher: class Dispatcher:
@ -36,8 +40,10 @@ 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.bot = None self.middlewares: List[BaseMiddleware] = []
self.on_started_func = None
self.bot: Bot = None
self.on_started_func: Callable = None
self.message_created = Event(update_type=UpdateType.MESSAGE_CREATED, router=self) self.message_created = Event(update_type=UpdateType.MESSAGE_CREATED, router=self)
self.bot_added = Event(update_type=UpdateType.BOT_ADDED, router=self) self.bot_added = Event(update_type=UpdateType.BOT_ADDED, router=self)
@ -78,7 +84,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 +111,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 +143,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,12 +231,10 @@ 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'Ошибка подключения, жду {CONNECTION_RETRY_DELAY} секунд')
await asyncio.sleep(CONNECTION_RETRY_DELAY)
except Exception as e: except Exception as e:
logger_dp.error(f'Общая ошибка при обработке событий: {e}') logger_dp.error(f'Общая ошибка при обработке событий: {e}')
@ -208,7 +250,7 @@ class Dispatcher:
await self.__ready(bot) await self.__ready(bot)
@app.post('/') @webhook_app.post('/')
async def _(request: Request): async def _(request: Request):
try: try:
event_json = await request.json() event_json = await request.json()
@ -224,7 +266,7 @@ class Dispatcher:
except Exception as e: except Exception as e:
logger_dp.error(f"Ошибка при обработке события: {event_json['update_type']}: {e}") logger_dp.error(f"Ошибка при обработке события: {event_json['update_type']}: {e}")
config = Config(app=app, host=host, port=port, log_level="critical") config = Config(app=webhook_app, host=host, port=port, log_level="critical")
server = Server(config) server = Server(config)
await server.serve() await server.serve()

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

@ -1,4 +1,4 @@
from typing import List, TYPE_CHECKING from typing import List, TYPE_CHECKING, Optional
from .types.edited_message import EditedMessage from .types.edited_message import EditedMessage
from ..types.message import NewMessageLink from ..types.message import NewMessageLink
@ -38,7 +38,7 @@ class EditMessage(BaseConnection):
attachments: List['Attachment'] = None, attachments: List['Attachment'] = None,
link: 'NewMessageLink' = None, link: 'NewMessageLink' = None,
notify: bool = True, notify: bool = True,
parse_mode: ParseMode = None parse_mode: Optional[ParseMode] = None
): ):
self.bot = bot self.bot = bot
self.message_id = message_id self.message_id = message_id

View File

@ -1,5 +1,4 @@
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ..methods.types.sended_callback import SendedCallback from ..methods.types.sended_callback import SendedCallback
@ -37,7 +36,7 @@ class SendCallback(BaseConnection):
self, self,
bot: 'Bot', bot: 'Bot',
callback_id: str, callback_id: str,
message: 'Message' = None, message: Message = None,
notification: str = None notification: str = None
): ):
self.bot = bot self.bot = bot

View File

@ -1,7 +1,7 @@
import asyncio import asyncio
from typing import List, TYPE_CHECKING from typing import List, TYPE_CHECKING, Optional
from json import loads as json_loads from json import loads as json_loads
@ -23,10 +23,10 @@ from ..loggers import logger_bot
if TYPE_CHECKING: if TYPE_CHECKING:
from ..bot import Bot from ..bot import Bot
RETRY_DELAY = 2
class UploadResponse: ATTEMPTS_COUNT = 5
token: str = None
class SendMessage(BaseConnection): class SendMessage(BaseConnection):
@ -38,7 +38,6 @@ class SendMessage(BaseConnection):
bot (Bot): Экземпляр бота для выполнения запроса. bot (Bot): Экземпляр бота для выполнения запроса.
chat_id (int, optional): Идентификатор чата, куда отправлять сообщение. chat_id (int, optional): Идентификатор чата, куда отправлять сообщение.
user_id (int, optional): Идентификатор пользователя, если нужно отправить личное сообщение. user_id (int, optional): Идентификатор пользователя, если нужно отправить личное сообщение.
disable_link_preview (bool, optional): Отключить предпросмотр ссылок. По умолчанию False.
text (str, optional): Текст сообщения. text (str, optional): Текст сообщения.
attachments (List[Attachment | InputMedia], optional): Список вложений к сообщению. attachments (List[Attachment | InputMedia], optional): Список вложений к сообщению.
link (NewMessageLink, optional): Связь с другим сообщением (например, ответ или пересылка). link (NewMessageLink, optional): Связь с другим сообщением (например, ответ или пересылка).
@ -51,17 +50,15 @@ class SendMessage(BaseConnection):
bot: 'Bot', bot: 'Bot',
chat_id: int = None, chat_id: int = None,
user_id: int = None, user_id: int = None,
disable_link_preview: bool = False,
text: str = None, text: str = None,
attachments: List[Attachment | InputMedia] = None, attachments: List[Attachment | InputMedia] = None,
link: NewMessageLink = None, link: NewMessageLink = None,
notify: bool = True, notify: bool = True,
parse_mode: ParseMode = None parse_mode: Optional[ParseMode] = None
): ):
self.bot = bot self.bot = bot
self.chat_id = chat_id self.chat_id = chat_id
self.user_id = user_id self.user_id = user_id
self.disable_link_preview = disable_link_preview
self.text = text self.text = text
self.attachments = attachments self.attachments = attachments
self.link = link self.link = link
@ -73,6 +70,9 @@ class SendMessage(BaseConnection):
att: InputMedia att: InputMedia
): ):
# очень нестабильный метод независящий от модуля
# ждем обновлений MAX API
""" """
Загружает файл вложения и формирует объект AttachmentUpload. Загружает файл вложения и формирует объект AttachmentUpload.
@ -129,7 +129,6 @@ class SendMessage(BaseConnection):
elif self.user_id: params['user_id'] = self.user_id elif self.user_id: params['user_id'] = self.user_id
json['text'] = self.text json['text'] = self.text
json['disable_link_preview'] = str(self.disable_link_preview).lower()
if self.attachments: if self.attachments:
@ -144,11 +143,11 @@ class SendMessage(BaseConnection):
json['attachments'].append(att.model_dump()) json['attachments'].append(att.model_dump())
if not self.link is None: json['link'] = self.link.model_dump() if not self.link is None: json['link'] = self.link.model_dump()
if not self.notify is None: json['notify'] = self.notify json['notify'] = self.notify
if not self.parse_mode is None: json['format'] = self.parse_mode.value if not self.parse_mode is None: json['format'] = self.parse_mode.value
response = None response = None
for attempt in range(5): for attempt in range(ATTEMPTS_COUNT):
response = await super().request( response = await super().request(
method=HTTPMethod.POST, method=HTTPMethod.POST,
path=ApiPath.MESSAGES, path=ApiPath.MESSAGES,
@ -159,8 +158,8 @@ class SendMessage(BaseConnection):
if isinstance(response, Error): if isinstance(response, Error):
if response.raw.get('code') == 'attachment.not.ready': if response.raw.get('code') == 'attachment.not.ready':
logger_bot.info(f'Ошибка при отправке загруженного медиа, попытка {attempt+1}, жду 2 секунды') logger_bot.info(f'Ошибка при отправке загруженного медиа, попытка {attempt+1}, жду {RETRY_DELAY} секунды')
await asyncio.sleep(2) await asyncio.sleep(RETRY_DELAY)
continue continue
return response return response

View File

@ -36,7 +36,10 @@ async def get_update_model(event: dict, bot: 'Bot'):
case UpdateType.MESSAGE_CALLBACK: case UpdateType.MESSAGE_CALLBACK:
event_object = MessageCallback(**event) event_object = MessageCallback(**event)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.callback.user event_object.from_user = event_object.callback.user
case UpdateType.MESSAGE_CHAT_CREATED: case UpdateType.MESSAGE_CHAT_CREATED:
@ -45,40 +48,59 @@ async def get_update_model(event: dict, bot: 'Bot'):
case UpdateType.MESSAGE_CREATED: case UpdateType.MESSAGE_CREATED:
event_object = MessageCreated(**event) event_object = MessageCreated(**event)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.message.sender event_object.from_user = event_object.message.sender
case UpdateType.MESSAGE_EDITED: case UpdateType.MESSAGE_EDITED:
event_object = MessageEdited(**event) event_object = MessageEdited(**event)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id)
event_object.chat = await bot.get_chat_by_id(event_object.message.recipient.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.message.sender event_object.from_user = event_object.message.sender
case UpdateType.MESSAGE_REMOVED: case UpdateType.MESSAGE_REMOVED:
event_object = MessageRemoved(**event) event_object = MessageRemoved(**event)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
event_object.from_user = await bot.get_chat_member( event_object.from_user = await bot.get_chat_member(
chat_id=event_object.chat_id, chat_id=event_object.chat_id,
user_id=event_object.user_id user_id=event_object.user_id
) ) if bot.auto_requests else None
case UpdateType.USER_ADDED: case UpdateType.USER_ADDED:
event_object = UserAdded(**event) event_object = UserAdded(**event)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.user event_object.from_user = event_object.user
case UpdateType.USER_REMOVED: case UpdateType.USER_REMOVED:
event_object = UserRemoved(**event) event_object = UserRemoved(**event)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
event_object.from_user = await bot.get_chat_member( event_object.from_user = await bot.get_chat_member(
chat_id=event_object.chat_id, chat_id=event_object.chat_id,
user_id=event_object.admin_id user_id=event_object.admin_id
) if event_object.admin_id else None ) if event_object.admin_id and \
bot.auto_requests else None
if event['update_type'] in (UpdateType.BOT_ADDED, if event['update_type'] in (UpdateType.BOT_ADDED,
UpdateType.BOT_REMOVED, UpdateType.BOT_REMOVED,
UpdateType.BOT_STARTED, UpdateType.BOT_STARTED,
UpdateType.CHAT_TITLE_CHANGED): UpdateType.CHAT_TITLE_CHANGED):
event_object.chat = await bot.get_chat_by_id(event_object.chat_id)
event_object.chat = await bot.get_chat_by_id(event_object.chat_id) \
if bot.auto_requests else None
event_object.from_user = event_object.user event_object.from_user = event_object.user
if hasattr(event_object, 'bot'): if hasattr(event_object, 'bot'):

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 ....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

View File

@ -169,21 +169,20 @@ class Message(BaseModel):
if TYPE_CHECKING: if TYPE_CHECKING:
bot: Optional[Bot] bot: Optional[Bot]
async def answer(self, async def answer(
self,
text: str = None, text: str = None,
disable_link_preview: bool = False,
attachments: List[Attachment] = None, attachments: List[Attachment] = None,
link: NewMessageLink = None, link: NewMessageLink = None,
notify: bool = True, notify: Optional[bool] = None,
parse_mode: ParseMode = None parse_mode: Optional[ParseMode] = None
): ):
""" """
Отправляет ответное сообщение. Отправляет сообщение (автозаполнение chat_id, user_id).
Args: Args:
text (str, optional): Текст ответа. Может быть None. text (str, optional): Текст ответа. Может быть None.
disable_link_preview (bool): Отключить предпросмотр ссылок. По умолчанию False.
attachments (List[Attachment], optional): Список вложений. Может быть None. attachments (List[Attachment], optional): Список вложений. Может быть None.
link (NewMessageLink, optional): Связь с другим сообщением. Может быть None. link (NewMessageLink, optional): Связь с другим сообщением. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True. notify (bool): Флаг отправки уведомления. По умолчанию True.
@ -197,12 +196,80 @@ class Message(BaseModel):
chat_id=self.recipient.chat_id, chat_id=self.recipient.chat_id,
user_id=self.recipient.user_id, user_id=self.recipient.user_id,
text=text, text=text,
disable_link_preview=disable_link_preview,
attachments=attachments, attachments=attachments,
link=link, link=link,
notify=notify, notify=notify,
parse_mode=parse_mode parse_mode=parse_mode
) )
async def reply(
self,
text: str = None,
attachments: List[Attachment] = None,
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
"""
Отправляет ответное сообщение (автозаполнение chat_id, user_id, link).
Args:
text (str, optional): Текст ответа. Может быть None.
attachments (List[Attachment], optional): Список вложений. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True.
parse_mode (ParseMode, optional): Режим форматирования текста. Может быть None.
Returns:
Any: Результат выполнения метода send_message бота.
"""
return await self.bot.send_message(
chat_id=self.recipient.chat_id,
user_id=self.recipient.user_id,
text=text,
attachments=attachments,
link=NewMessageLink(
type=MessageLinkType.REPLY,
mid=self.body.mid
),
notify=notify,
parse_mode=parse_mode
)
async def forward(
self,
chat_id,
user_id: int = None,
attachments: List[Attachment] = None,
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
"""
Пересылает отправленное сообщение в указанный чат (автозаполнение link).
Args:
chat_id (int): ID чата для отправки (обязателен, если не указан user_id)
user_id (int): ID пользователя для отправки (обязателен, если не указан chat_id). По умолчанию None
attachments (List[Attachment], optional): Список вложений. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True.
parse_mode (ParseMode, optional): Режим форматирования текста. Может быть None.
Returns:
Any: Результат выполнения метода send_message бота.
"""
return await self.bot.send_message(
chat_id=chat_id,
user_id=user_id,
attachments=attachments,
link=NewMessageLink(
type=MessageLinkType.FORWARD,
mid=self.body.mid
),
notify=notify,
parse_mode=parse_mode
)
async def edit( async def edit(
self, self,
@ -210,7 +277,7 @@ class Message(BaseModel):
attachments: List[Attachment] = None, attachments: List[Attachment] = None,
link: NewMessageLink = None, link: NewMessageLink = None,
notify: bool = True, notify: bool = True,
parse_mode: ParseMode = None parse_mode: Optional[ParseMode] = None
): ):
""" """

View File

@ -90,7 +90,7 @@ class MessageCallback(Update):
new_text: str = None, new_text: str = None,
link: NewMessageLink = None, link: NewMessageLink = None,
notify: bool = True, notify: bool = True,
format: ParseMode = None, format: Optional[ParseMode] = None,
): ):
""" """

View File

@ -1,4 +1,4 @@
from maxapi.types.attachments.buttons import InlineButtonUnion from ..types.attachments.buttons import InlineButtonUnion
from ..enums.attachment import AttachmentType from ..enums.attachment import AttachmentType
from ..types.attachments.attachment import Attachment, ButtonsPayload from ..types.attachments.attachment import Attachment, ButtonsPayload

View File

@ -1,6 +1,6 @@
[project] [project]
name = "maxapi" name = "maxapi"
version = "0.8.3" version = "0.8.5"
description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX" description = "Библиотека для разработки чат-ботов с помощью API мессенджера MAX"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@ -18,6 +18,7 @@ dependencies = [
"magic_filter>=1.0.0", "magic_filter>=1.0.0",
"pydantic>=1.8.0", "pydantic>=1.8.0",
"uvicorn>=0.15.0", "uvicorn>=0.15.0",
"aiofiles==24.1.0",
] ]
[project.urls] [project.urls]

View File

@ -1,3 +1,3 @@
from setuptools import setup, find_packages from setuptools import setup
setup() setup()