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
test.py
__pycache__/
*.py[cod]
*$py.class

View File

@ -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
- ✅ Роутеры
- ✅ Билдер инлайн клавиатур
- ✅ Простая загрузка медиафайлов

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 typing import Any, Dict, List, 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 typing import Any, Dict, List, Optional, TYPE_CHECKING
from .connection.base import BaseConnection
from .enums.parse_mode import ParseMode
from .enums.sender_action import SenderAction
from .enums.upload_type import UploadType
from .types.message import Message
from .types.attachments.attachment import Attachment
from .types.attachments.image import PhotoAttachmentRequestPayload
from .types.message import Messages, NewMessageLink
from .types.users import ChatAdmin, User
from .types.command import BotCommand
from .methods.add_admin_chat import AddAdminChat
from .methods.add_members_chat import AddMembersChat
from .methods.change_info import ChangeInfo
from .methods.delete_bot_from_chat import DeleteMeFromMessage
from .methods.delete_chat import DeleteChat
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_members_chat import AddedMembersChat
from .methods.types.deleted_bot_from_chat import DeletedBotFromChat
from .methods.types.deleted_chat import DeletedChat
from .methods.types.deleted_message import DeletedMessage
from .methods.types.deleted_pin_message import DeletedPinMessage
from .methods.types.edited_message import EditedMessage
from .methods.types.getted_list_admin_chat import GettedListAdminChat
from .methods.types.getted_members_chat import GettedMembersChat
from .methods.types.getted_pineed_message import GettedPin
from .methods.types.getted_upload_url import GettedUploadUrl
from .methods.types.pinned_message import PinnedMessage
from .methods.types.removed_admin import RemovedAdmin
from .methods.types.removed_member_chat import RemovedMemberChat
from .methods.types.sended_action import SendedAction
from .methods.types.sended_callback import SendedCallback
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
from .methods.types.added_admin_chat import AddedListAdminChat
from .methods.types.added_members_chat import AddedMembersChat
from .methods.types.deleted_bot_from_chat import DeletedBotFromChat
from .methods.types.deleted_chat import DeletedChat
from .methods.types.deleted_message import DeletedMessage
from .methods.types.deleted_pin_message import DeletedPinMessage
from .methods.types.edited_message import EditedMessage
from .methods.types.getted_list_admin_chat import GettedListAdminChat
from .methods.types.getted_members_chat import GettedMembersChat
from .methods.types.getted_pineed_message import GettedPin
from .methods.types.getted_upload_url import GettedUploadUrl
from .methods.types.pinned_message import PinnedMessage
from .methods.types.removed_admin import RemovedAdmin
from .methods.types.removed_member_chat import RemovedMemberChat
from .methods.types.sended_action import SendedAction
from .methods.types.sended_callback import SendedCallback
from .methods.types.sended_message import SendedMessage
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 parse_mode: Форматирование по умолчанию
:param notify: Отключение уведомлений при отправке сообщений (по умолчанию игнорируется) (не работает на стороне MAX)
:param auto_requests: Автоматическое заполнение полей chat и from_user в Update
с помощью API запросов если они не заложены как полноценные объекты в Update (по умолчанию True, при False chat и from_user в некоторых событиях будут выдавать None)
"""
super().__init__()
@ -89,27 +99,35 @@ class Bot(BaseConnection):
self.params = {'access_token': self.__token}
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(
self,
chat_id: int = None,
user_id: int = None,
disable_link_preview: bool = False,
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
) -> SendedMessage:
"""Отправляет сообщение в чат или пользователю.
:param chat_id: ID чата для отправки (обязателен, если не указан user_id)
:param user_id: ID пользователя для отправки (обязателен, если не указан chat_id)
:param disable_link_preview: Отключить предпросмотр ссылок (по умолчанию False)
:param text: Текст сообщения
:param attachments: Список вложений к сообщению
:param link: Данные ссылки сообщения
:param notify: Отправлять уведомление получателю (по умолчанию True)
:param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:param parse_mode: Режим форматирования текста
:return: Объект отправленного сообщения
@ -119,12 +137,11 @@ class Bot(BaseConnection):
bot=self,
chat_id=chat_id,
user_id=user_id,
disable_link_preview=disable_link_preview,
text=text,
attachments=attachments,
link=link,
notify=notify,
parse_mode=parse_mode
notify=self._resolve_notify(notify),
parse_mode=self._resolve_parse_mode(notify)
).request()
async def send_action(
@ -153,8 +170,8 @@ class Bot(BaseConnection):
text: str = None,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
) -> EditedMessage:
"""Редактирует существующее сообщение.
@ -163,7 +180,7 @@ class Bot(BaseConnection):
:param text: Новый текст сообщения
:param attachments: Новые вложения
:param link: Новая ссылка сообщения
:param notify: Уведомлять получателя об изменении (по умолчанию True)
:param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:param parse_mode: Режим форматирования текста
:return: Объект отредактированного сообщения
@ -175,8 +192,8 @@ class Bot(BaseConnection):
text=text,
attachments=attachments,
link=link,
notify=notify,
parse_mode=parse_mode
notify=self._resolve_notify(notify),
parse_mode=self._resolve_parse_mode(notify)
).request()
async def delete_message(
@ -364,7 +381,7 @@ class Bot(BaseConnection):
icon: PhotoAttachmentRequestPayload = None,
title: str = None,
pin: str = None,
notify: bool = True,
notify: Optional[bool] = None,
) -> Chat:
"""Редактирует параметры чата.
@ -373,7 +390,7 @@ class Bot(BaseConnection):
:param icon: Данные иконки чата
:param title: Новый заголовок чата
:param pin: ID сообщения для закрепления
:param notify: Уведомлять участников (по умолчанию True)
:param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:return: Обновленный объект чата
"""
@ -384,7 +401,7 @@ class Bot(BaseConnection):
icon=icon,
title=title,
pin=pin,
notify=notify
notify=self._resolve_notify(notify),
).request()
async def get_video(
@ -407,7 +424,7 @@ class Bot(BaseConnection):
async def send_callback(
self,
callback_id: str,
message: 'Message' = None,
message: Message = None,
notification: str = None
) -> SendedCallback:
@ -431,14 +448,14 @@ class Bot(BaseConnection):
self,
chat_id: int,
message_id: str,
notify: bool = True
notify: Optional[bool] = None
) -> PinnedMessage:
"""Закрепляет сообщение в чате.
:param chat_id: ID чата
:param message_id: ID сообщения
:param notify: Уведомлять участников (по умолчанию True)
:param notify: Отправлять уведомление получателю (по умолчанию берется значение из бота)
:return: Закрепленное сообщение
"""
@ -447,7 +464,7 @@ class Bot(BaseConnection):
bot=self,
chat_id=chat_id,
message_id=message_id,
notify=notify
notify=self._resolve_notify(notify),
).request()
async def delete_pin_message(

View File

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

View File

@ -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__}`')

View File

@ -1,6 +1,23 @@
from typing import Any, Dict
from ..types.updates import UpdateUnion
class BaseMiddleware:
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.message import NewMessageLink
@ -38,7 +38,7 @@ class EditMessage(BaseConnection):
attachments: List['Attachment'] = None,
link: 'NewMessageLink' = None,
notify: bool = True,
parse_mode: ParseMode = None
parse_mode: Optional[ParseMode] = None
):
self.bot = bot
self.message_id = message_id

View File

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

View File

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

View File

@ -36,7 +36,10 @@ async def get_update_model(event: dict, bot: 'Bot'):
case UpdateType.MESSAGE_CALLBACK:
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
case UpdateType.MESSAGE_CHAT_CREATED:
@ -45,40 +48,59 @@ async def get_update_model(event: dict, bot: 'Bot'):
case UpdateType.MESSAGE_CREATED:
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
case UpdateType.MESSAGE_EDITED:
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
case UpdateType.MESSAGE_REMOVED:
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(
chat_id=event_object.chat_id,
user_id=event_object.user_id
)
) if bot.auto_requests else None
case UpdateType.USER_ADDED:
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
case UpdateType.USER_REMOVED:
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(
chat_id=event_object.chat_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,
UpdateType.BOT_REMOVED,
UpdateType.BOT_STARTED,
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
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.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,

View File

@ -1,5 +1,7 @@
from typing import Optional
from ....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

View File

@ -169,21 +169,20 @@ class Message(BaseModel):
if TYPE_CHECKING:
bot: Optional[Bot]
async def answer(self,
async def answer(
self,
text: str = None,
disable_link_preview: bool = False,
attachments: List[Attachment] = None,
link: NewMessageLink = None,
notify: bool = True,
parse_mode: ParseMode = None
notify: Optional[bool] = None,
parse_mode: Optional[ParseMode] = None
):
"""
Отправляет ответное сообщение.
Отправляет сообщение (автозаполнение chat_id, user_id).
Args:
text (str, optional): Текст ответа. Может быть None.
disable_link_preview (bool): Отключить предпросмотр ссылок. По умолчанию False.
attachments (List[Attachment], optional): Список вложений. Может быть None.
link (NewMessageLink, optional): Связь с другим сообщением. Может быть None.
notify (bool): Флаг отправки уведомления. По умолчанию True.
@ -197,12 +196,80 @@ class Message(BaseModel):
chat_id=self.recipient.chat_id,
user_id=self.recipient.user_id,
text=text,
disable_link_preview=disable_link_preview,
attachments=attachments,
link=link,
notify=notify,
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(
self,
@ -210,7 +277,7 @@ class Message(BaseModel):
attachments: List[Attachment] = None,
link: NewMessageLink = None,
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,
link: NewMessageLink = None,
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 ..types.attachments.attachment import Attachment, ButtonsPayload

View File

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

View File

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