diff --git a/maxapi/bot.py b/maxapi/bot.py index b195e11..6e42a8a 100644 --- a/maxapi/bot.py +++ b/maxapi/bot.py @@ -1,7 +1,9 @@ from __future__ import annotations from datetime import datetime -from typing import Any, Dict, List, Optional, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING + +from .client.default import DefaultConnectionProperties from .types.input_media import InputMedia, InputMediaBuffer @@ -82,6 +84,7 @@ class Bot(BaseConnection): parse_mode: Optional[ParseMode] = None, notify: Optional[bool] = None, auto_requests: bool = True, + default_connection: Optional[DefaultConnectionProperties] = None ): """ @@ -91,22 +94,24 @@ class Bot(BaseConnection): :param parse_mode: Форматирование по умолчанию :param notify: Отключение уведомлений при отправке сообщений (по умолчанию игнорируется) (не работает на стороне MAX) :param auto_requests: Автоматическое заполнение полей chat и from_user в Update + :param default_connection: Настройки aiohttp с помощью API запросов если они не заложены как полноценные объекты в Update (по умолчанию True, при False chat и from_user в некоторых событиях будут выдавать None) """ super().__init__() self.bot = self + self.default_connection = default_connection or DefaultConnectionProperties() self.__token = token - self.params = {'access_token': self.__token} + self.params: Dict[str, Any] = {'access_token': self.__token} self.marker_updates = None self.parse_mode = parse_mode self.notify = notify self.auto_requests = auto_requests - self._me: User = None + self._me: User | None = None @property def me(self): @@ -120,11 +125,11 @@ class Bot(BaseConnection): async def send_message( self, - chat_id: int = None, - user_id: int = None, - text: str = None, - attachments: List[Attachment | InputMedia | InputMediaBuffer] = None, - link: NewMessageLink = None, + chat_id: Optional[int] = None, + user_id: Optional[int] = None, + text: Optional[str] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, + link: Optional[NewMessageLink] = None, notify: Optional[bool] = None, parse_mode: Optional[ParseMode] = None ) -> SendedMessage: @@ -152,11 +157,11 @@ class Bot(BaseConnection): link=link, notify=self._resolve_notify(notify), parse_mode=self._resolve_parse_mode(parse_mode) - ).request() + ).fetch() async def send_action( self, - chat_id: int = None, + chat_id: Optional[int] = None, action: SenderAction = SenderAction.TYPING_ON ) -> SendedAction: @@ -173,14 +178,14 @@ class Bot(BaseConnection): bot=self, chat_id=chat_id, action=action - ).request() + ).fetch() async def edit_message( self, message_id: str, - text: str = None, - attachments: List[Attachment | InputMedia | InputMediaBuffer] = None, - link: NewMessageLink = None, + text: Optional[str] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, + link: Optional[NewMessageLink] = None, notify: Optional[bool] = None, parse_mode: Optional[ParseMode] = None ) -> EditedMessage: @@ -206,7 +211,7 @@ class Bot(BaseConnection): link=link, notify=self._resolve_notify(notify), parse_mode=self._resolve_parse_mode(parse_mode) - ).request() + ).fetch() async def delete_message( self, @@ -224,7 +229,7 @@ class Bot(BaseConnection): return await DeleteMessage( bot=self, message_id=message_id, - ).request() + ).fetch() async def delete_chat( self, @@ -242,14 +247,14 @@ class Bot(BaseConnection): return await DeleteChat( bot=self, chat_id=chat_id, - ).request() + ).fetch() async def get_messages( self, - chat_id: int = None, - message_ids: List[str] = None, - from_time: datetime | int = None, - to_time: datetime | int = None, + chat_id: Optional[int] = None, + message_ids: Optional[List[str]] = None, + from_time: Optional[Union[datetime, int]] = None, + to_time: Optional[Union[datetime, int]] = None, count: int = 50, ) -> Messages: @@ -272,7 +277,7 @@ class Bot(BaseConnection): from_time=from_time, to_time=to_time, count=count - ).request() + ).fetch() async def get_message( self, @@ -299,7 +304,7 @@ class Bot(BaseConnection): :return: Объект пользователя бота """ - return await GetMe(self).request() + return await GetMe(self).fetch() async def get_pin_message( self, @@ -317,14 +322,14 @@ class Bot(BaseConnection): return await GetPinnedMessage( bot=self, chat_id=chat_id - ).request() + ).fetch() async def change_info( self, - name: str = None, - description: str = None, - commands: List[BotCommand] = None, - photo: Dict[str, Any] = None + name: Optional[str] = None, + description: Optional[str] = None, + commands: Optional[List[BotCommand]] = None, + photo: Optional[Dict[str, Any]] = None ) -> User: """ @@ -344,12 +349,12 @@ class Bot(BaseConnection): description=description, commands=commands, photo=photo - ).request() + ).fetch() async def get_chats( self, count: int = 50, - marker: int = None + marker: Optional[int] = None ) -> Chats: """ @@ -365,7 +370,7 @@ class Bot(BaseConnection): bot=self, count=count, marker=marker - ).request() + ).fetch() async def get_chat_by_link( self, @@ -380,7 +385,7 @@ class Bot(BaseConnection): :return: Объект чата """ - return await GetChatByLink(bot=self, link=link).request() + return await GetChatByLink(bot=self, link=link).fetch() async def get_chat_by_id( self, @@ -395,14 +400,14 @@ class Bot(BaseConnection): :return: Объект чата """ - return await GetChatById(bot=self, id=id).request() + return await GetChatById(bot=self, id=id).fetch() async def edit_chat( self, chat_id: int, - icon: PhotoAttachmentRequestPayload = None, - title: str = None, - pin: str = None, + icon: Optional[PhotoAttachmentRequestPayload] = None, + title: Optional[str] = None, + pin: Optional[str] = None, notify: Optional[bool] = None, ) -> Chat: @@ -425,7 +430,7 @@ class Bot(BaseConnection): title=title, pin=pin, notify=self._resolve_notify(notify), - ).request() + ).fetch() async def get_video( self, @@ -443,13 +448,13 @@ class Bot(BaseConnection): return await GetVideo( bot=self, video_token=video_token - ).request() + ).fetch() async def send_callback( self, callback_id: str, - message: Message = None, - notification: str = None + message: Optional[Message] = None, + notification: Optional[str] = None ) -> SendedCallback: """ @@ -467,7 +472,7 @@ class Bot(BaseConnection): callback_id=callback_id, message=message, notification=notification - ).request() + ).fetch() async def pin_message( self, @@ -491,7 +496,7 @@ class Bot(BaseConnection): chat_id=chat_id, message_id=message_id, notify=self._resolve_notify(notify), - ).request() + ).fetch() async def delete_pin_message( self, @@ -509,7 +514,7 @@ class Bot(BaseConnection): return await DeletePinMessage( bot=self, chat_id=chat_id, - ).request() + ).fetch() async def get_me_from_chat( self, @@ -527,7 +532,7 @@ class Bot(BaseConnection): return await GetMeFromChat( bot=self, chat_id=chat_id, - ).request() + ).fetch() async def delete_me_from_chat( self, @@ -545,7 +550,7 @@ class Bot(BaseConnection): return await DeleteMeFromMessage( bot=self, chat_id=chat_id, - ).request() + ).fetch() async def get_list_admin_chat( self, @@ -563,13 +568,13 @@ class Bot(BaseConnection): return await GetListAdminChat( bot=self, chat_id=chat_id, - ).request() + ).fetch() async def add_list_admin_chat( self, chat_id: int, admins: List[ChatAdmin], - marker: int = None + marker: Optional[int] = None ) -> AddedListAdminChat: """ @@ -587,7 +592,7 @@ class Bot(BaseConnection): chat_id=chat_id, admins=admins, marker=marker, - ).request() + ).fetch() async def remove_admin( self, @@ -608,14 +613,14 @@ class Bot(BaseConnection): bot=self, chat_id=chat_id, user_id=user_id, - ).request() + ).fetch() async def get_chat_members( self, chat_id: int, - user_ids: List[int] = None, - marker: int = None, - count: int = None, + user_ids: Optional[List[int]] = None, + marker: Optional[int] = None, + count: Optional[int] = None, ) -> GettedMembersChat: """ @@ -635,13 +640,13 @@ class Bot(BaseConnection): user_ids=user_ids, marker=marker, count=count, - ).request() + ).fetch() async def get_chat_member( self, chat_id: int, user_id: int, - ) -> GettedMembersChat: + ) -> Optional[ChatMember]: """ Получает участника чата. @@ -659,11 +664,13 @@ class Bot(BaseConnection): if members.members: return members.members[0] + + return None async def add_chat_members( self, chat_id: int, - user_ids: List[str], + user_ids: List[int], ) -> AddedMembersChat: """ @@ -679,7 +686,7 @@ class Bot(BaseConnection): bot=self, chat_id=chat_id, user_ids=user_ids, - ).request() + ).fetch() async def kick_chat_member( self, @@ -703,11 +710,11 @@ class Bot(BaseConnection): chat_id=chat_id, user_id=user_id, block=block, - ).request() + ).fetch() async def get_updates( self, - ) -> UpdateUnion: + ) -> Dict: """ Получает обновления для бота. @@ -717,7 +724,7 @@ class Bot(BaseConnection): return await GetUpdates( bot=self, - ).request() + ).fetch() async def get_upload_url( self, @@ -735,7 +742,7 @@ class Bot(BaseConnection): return await GetUploadURL( bot=self, type=type - ).request() + ).fetch() async def set_my_commands( self, @@ -753,7 +760,7 @@ class Bot(BaseConnection): return await ChangeInfo( bot=self, commands=list(commands) - ).request() + ).fetch() async def download_file( self, @@ -777,4 +784,4 @@ class Bot(BaseConnection): path=path, media_url=url, media_token=token - ).request() \ No newline at end of file + ).fetch() \ No newline at end of file diff --git a/maxapi/client/default.py b/maxapi/client/default.py new file mode 100644 index 0000000..7c5dfea --- /dev/null +++ b/maxapi/client/default.py @@ -0,0 +1,10 @@ + + +from aiohttp import ClientTimeout + + +class DefaultConnectionProperties: + + def __init__(self, timeout: int = 5 * 30, sock_connect: int = 30, **kwargs): + self.timeout = ClientTimeout(total=timeout, sock_connect=sock_connect) + self.kwargs = kwargs \ No newline at end of file diff --git a/maxapi/connection/base.py b/maxapi/connection/base.py index 98c49be..305224f 100644 --- a/maxapi/connection/base.py +++ b/maxapi/connection/base.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import os import mimetypes -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Optional from uuid import uuid4 import aiofiles @@ -18,7 +20,7 @@ from ..enums.http_method import HTTPMethod from ..enums.api_path import ApiPath from ..enums.upload_type import UploadType -from ..loggers import logger_bot, logger_connection +from ..loggers import logger_bot if TYPE_CHECKING: from ..bot import Bot @@ -36,15 +38,15 @@ class BaseConnection: API_URL = 'https://botapi.max.ru' - def __init__(self): - self.bot: 'Bot' = None - self.session: ClientSession = None + def __init__(self) -> None: + self.bot: Optional[Bot] = None + self.session: Optional[ClientSession] = None async def request( self, method: HTTPMethod, - path: ApiPath, - model: BaseModel = None, + path: ApiPath | str, + model: BaseModel | Any = None, is_return_raw: bool = False, **kwargs ): @@ -64,8 +66,14 @@ class BaseConnection: - dict (если is_return_raw=True) """ + assert self.bot is not None + if not self.bot.session: - self.bot.session = ClientSession(self.bot.API_URL) + self.bot.session = ClientSession( + base_url=self.bot.API_URL, + timeout=self.bot.default_connection.timeout, + **self.bot.default_connection.kwargs + ) try: r = await self.bot.session.request( @@ -90,7 +98,7 @@ class BaseConnection: if is_return_raw: return raw - model = model(**raw) + model = model(**raw) # type: ignore if hasattr(model, 'message'): attr = getattr(model, 'message') diff --git a/maxapi/context/__init__.py b/maxapi/context/__init__.py index 0dbaab7..8ab930e 100644 --- a/maxapi/context/__init__.py +++ b/maxapi/context/__init__.py @@ -1,6 +1,6 @@ import asyncio -from typing import Any, Dict +from typing import Any, Dict, Optional, Union from ..context.state_machine import State, StatesGroup @@ -19,7 +19,7 @@ class MemoryContext: self.chat_id = chat_id self.user_id = user_id self._context: Dict[str, Any] = {} - self._state: State | None = None + self._state: State | str | None = None self._lock = asyncio.Lock() async def get_data(self) -> dict[str, Any]: @@ -58,7 +58,7 @@ class MemoryContext: async with self._lock: self._context.update(kwargs) - async def set_state(self, state: State | str = None): + async def set_state(self, state: Optional[Union[State, str]] = None): """ Устанавливает новое состояние. diff --git a/maxapi/dispatcher.py b/maxapi/dispatcher.py index b6e815d..a95a864 100644 --- a/maxapi/dispatcher.py +++ b/maxapi/dispatcher.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from typing import Any, Callable, Dict, List, TYPE_CHECKING +from typing import Any, Callable, Dict, List, TYPE_CHECKING, Optional, cast from fastapi import FastAPI, Request from fastapi.responses import JSONResponse @@ -45,7 +45,7 @@ class Dispatcher: применение middleware, фильтров и вызов соответствующих обработчиков. """ - def __init__(self): + def __init__(self) -> None: """ Инициализация диспетчера. @@ -53,12 +53,12 @@ class Dispatcher: self.event_handlers: List[Handler] = [] self.contexts: List[MemoryContext] = [] - self.routers: List[Router] = [] + self.routers: List[Router | Dispatcher] = [] self.filters: List[MagicFilter] = [] self.middlewares: List[BaseMiddleware] = [] - self.bot: Bot = None - self.on_started_func: Callable = None + self.bot: Optional[Bot] = None + self.on_started_func: Optional[Callable] = None self.message_created = Event(update_type=UpdateType.MESSAGE_CREATED, router=self) self.bot_added = Event(update_type=UpdateType.BOT_ADDED, router=self) @@ -249,18 +249,18 @@ class Dispatcher: while True: try: - events = await self.bot.get_updates() + events: Dict = await self.bot.get_updates() # type: ignore if isinstance(events, Error): logger_dp.info(f'Ошибка при получении обновлений: {events}, жду {GET_UPDATES_RETRY_DELAY} секунд') await asyncio.sleep(GET_UPDATES_RETRY_DELAY) continue - self.bot.marker_updates = events.get('marker') - + self.bot.marker_updates = events.get('marker') # type: ignore + processed_events = await process_update_request( events=events, - bot=self.bot + bot=self.bot # type: ignore ) for event in processed_events: @@ -270,7 +270,7 @@ class Dispatcher: logger_dp.error(f'Ошибка подключения, жду {CONNECTION_RETRY_DELAY} секунд') await asyncio.sleep(CONNECTION_RETRY_DELAY) except Exception as e: - logger_dp.error(f'Общая ошибка при обработке событий: {e}') + logger_dp.error(f'Общая ошибка при обработке событий: {e.__class__} - {e}') async def handle_webhook(self, bot: Bot, host: str = '0.0.0.0', port: int = 8080): @@ -289,7 +289,7 @@ class Dispatcher: event_object = await process_update_webhook( event_json=event_json, - bot=self.bot + bot=self.bot # type: ignore ) await self.handle(event_object) diff --git a/maxapi/enums/attachment.py b/maxapi/enums/attachment.py index d1d796d..17fc549 100644 --- a/maxapi/enums/attachment.py +++ b/maxapi/enums/attachment.py @@ -16,4 +16,5 @@ class AttachmentType(str, Enum): STICKER = 'sticker' CONTACT = 'contact' INLINE_KEYBOARD = 'inline_keyboard' - LOCATION = 'location' \ No newline at end of file + LOCATION = 'location' + SHARE = 'share' \ No newline at end of file diff --git a/maxapi/exceptions/max.py b/maxapi/exceptions/max.py index e424567..fc9fa42 100644 --- a/maxapi/exceptions/max.py +++ b/maxapi/exceptions/max.py @@ -1,3 +1,11 @@ class MaxConnection(BaseException): + ... + + +class MaxUploadFileFailed(BaseException): + ... + + +class MaxIconParamsException(BaseException): ... \ No newline at end of file diff --git a/maxapi/methods/add_admin_chat.py b/maxapi/methods/add_admin_chat.py index b0bb2cf..bde3f98 100644 --- a/maxapi/methods/add_admin_chat.py +++ b/maxapi/methods/add_admin_chat.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Any, Dict, List, Optional from .types.added_admin_chat import AddedListAdminChat from ..types.users import ChatAdmin @@ -30,14 +30,14 @@ class AddAdminChat(BaseConnection): bot: 'Bot', chat_id: int, admins: List[ChatAdmin], - marker: int = None + marker: Optional[int] = None ): self.bot = bot self.chat_id = chat_id self.admins = admins self.marker = marker - async def request(self) -> AddedListAdminChat: + async def fetch(self) -> AddedListAdminChat: """ Выполняет HTTP POST запрос для добавления администраторов в чат. @@ -48,7 +48,9 @@ class AddAdminChat(BaseConnection): AddedListAdminChat: Результат операции с информацией об успешности. """ - json = {} + assert self.bot is not None + + json: Dict[str, Any] = {} json['admins'] = [admin.model_dump() for admin in self.admins] json['marker'] = self.marker diff --git a/maxapi/methods/add_members_chat.py b/maxapi/methods/add_members_chat.py index 1bd7c8a..7e76b64 100644 --- a/maxapi/methods/add_members_chat.py +++ b/maxapi/methods/add_members_chat.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Any, Dict, List from ..methods.types.added_members_chat import AddedMembersChat @@ -34,7 +34,7 @@ class AddMembersChat(BaseConnection): self.chat_id = chat_id self.user_ids = user_ids - async def request(self) -> AddedMembersChat: + async def fetch(self) -> AddedMembersChat: """ Отправляет POST-запрос на добавление пользователей в чат. @@ -45,7 +45,9 @@ class AddMembersChat(BaseConnection): AddedMembersChat: Результат операции с информацией об успешности добавления. """ - json = {} + assert self.bot is not None + + json: Dict[str, Any] = {} json['user_ids'] = self.user_ids diff --git a/maxapi/methods/change_info.py b/maxapi/methods/change_info.py index 13b61a7..d8076f5 100644 --- a/maxapi/methods/change_info.py +++ b/maxapi/methods/change_info.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, TYPE_CHECKING +from typing import Any, Dict, List, TYPE_CHECKING, Optional from ..types.users import User from ..types.command import BotCommand @@ -29,10 +29,10 @@ class ChangeInfo(BaseConnection): def __init__( self, bot: 'Bot', - name: str = None, - description: str = None, - commands: List[BotCommand] = None, - photo: Dict[str, Any] = None + name: Optional[str] = None, + description: Optional[str] = None, + commands: Optional[List[BotCommand]] = None, + photo: Optional[Dict[str, Any]] = None ): self.bot = bot self.name = name @@ -40,7 +40,7 @@ class ChangeInfo(BaseConnection): self.commands = commands self.photo = photo - async def request(self) -> User: + async def fetch(self) -> User: """Отправляет запрос на изменение информации о боте. @@ -48,7 +48,9 @@ class ChangeInfo(BaseConnection): User: Объект с обновленными данными бота """ - json = {} + assert self.bot is not None + + json: Dict[str, Any] = {} if self.name: json['name'] = self.name if self.description: json['description'] = self.description diff --git a/maxapi/methods/delete_bot_from_chat.py b/maxapi/methods/delete_bot_from_chat.py index 6c475d0..8cfe2f9 100644 --- a/maxapi/methods/delete_bot_from_chat.py +++ b/maxapi/methods/delete_bot_from_chat.py @@ -30,7 +30,7 @@ class DeleteMeFromMessage(BaseConnection): self.bot = bot self.chat_id = chat_id - async def request(self) -> DeletedBotFromChat: + async def fetch(self) -> DeletedBotFromChat: """ Отправляет DELETE-запрос для удаления бота из чата. @@ -39,6 +39,7 @@ class DeleteMeFromMessage(BaseConnection): DeletedBotFromChat: Результат операции удаления. """ + assert self.bot is not None return await super().request( method=HTTPMethod.DELETE, path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.MEMBERS + ApiPath.ME, diff --git a/maxapi/methods/delete_chat.py b/maxapi/methods/delete_chat.py index 10b9107..017241c 100644 --- a/maxapi/methods/delete_chat.py +++ b/maxapi/methods/delete_chat.py @@ -29,7 +29,7 @@ class DeleteChat(BaseConnection): self.bot = bot self.chat_id = chat_id - async def request(self) -> DeletedChat: + async def fetch(self) -> DeletedChat: """ Отправляет DELETE-запрос для удаления указанного чата. @@ -38,6 +38,7 @@ class DeleteChat(BaseConnection): DeletedChat: Результат операции удаления чата. """ + assert self.bot is not None return await super().request( method=HTTPMethod.DELETE, path=ApiPath.CHATS.value + '/' + str(self.chat_id), diff --git a/maxapi/methods/delete_message.py b/maxapi/methods/delete_message.py index bb56776..b5ac682 100644 --- a/maxapi/methods/delete_message.py +++ b/maxapi/methods/delete_message.py @@ -29,7 +29,7 @@ class DeleteMessage(BaseConnection): self.bot = bot self.message_id = message_id - async def request(self) -> DeletedMessage: + async def fetch(self) -> DeletedMessage: """ Выполняет DELETE-запрос для удаления сообщения. @@ -40,6 +40,7 @@ class DeleteMessage(BaseConnection): DeletedMessage: Результат операции удаления сообщения. """ + assert self.bot is not None params = self.bot.params.copy() params['message_id'] = self.message_id diff --git a/maxapi/methods/delete_pin_message.py b/maxapi/methods/delete_pin_message.py index dc7276f..6c88331 100644 --- a/maxapi/methods/delete_pin_message.py +++ b/maxapi/methods/delete_pin_message.py @@ -19,18 +19,18 @@ class DeletePinMessage(BaseConnection): Args: bot (Bot): Экземпляр бота для выполнения запроса. - chat_id (str): Идентификатор чата, из которого нужно удалить закреплённое сообщение. + chat_id (int): Идентификатор чата, из которого нужно удалить закреплённое сообщение. """ def __init__( self, bot: 'Bot', - chat_id: str, + chat_id: int, ): self.bot = bot self.chat_id = chat_id - async def request(self) -> DeletedPinMessage: + async def fetch(self) -> DeletedPinMessage: """ Выполняет DELETE-запрос для удаления закреплённого сообщения. @@ -38,7 +38,7 @@ class DeletePinMessage(BaseConnection): Returns: DeletedPinMessage: Результат операции удаления закреплённого сообщения. """ - + assert self.bot is not None return await super().request( method=HTTPMethod.DELETE, path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.PIN, diff --git a/maxapi/methods/download_media.py b/maxapi/methods/download_media.py index 3d45dc7..2a5d5f3 100644 --- a/maxapi/methods/download_media.py +++ b/maxapi/methods/download_media.py @@ -36,7 +36,7 @@ class DownloadMedia(BaseConnection): self.media_url = media_url self.media_token = media_token - async def request(self) -> int: + async def fetch(self) -> int: """ Выполняет GET-запрос для скачивания медиафайла diff --git a/maxapi/methods/edit_chat.py b/maxapi/methods/edit_chat.py index a8e4da9..118966a 100644 --- a/maxapi/methods/edit_chat.py +++ b/maxapi/methods/edit_chat.py @@ -1,9 +1,11 @@ from logging import getLogger -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Optional from collections import Counter +from ..exceptions.max import MaxIconParamsException + from ..types.attachments.image import PhotoAttachmentRequestPayload from ..types.chats import Chat @@ -37,10 +39,10 @@ class EditChat(BaseConnection): self, bot: 'Bot', chat_id: int, - icon: PhotoAttachmentRequestPayload = None, - title: str = None, - pin: str = None, - notify: bool = True, + icon: Optional[PhotoAttachmentRequestPayload] = None, + title: Optional[str] = None, + pin: Optional[str] = None, + notify: Optional[bool] = None, ): self.bot = bot self.chat_id = chat_id @@ -49,7 +51,7 @@ class EditChat(BaseConnection): self.pin = pin self.notify = notify - async def request(self) -> Chat: + async def fetch(self) -> Chat: """ Выполняет PATCH-запрос для обновления параметров чата. @@ -62,7 +64,8 @@ class EditChat(BaseConnection): Chat: Обновлённый объект чата. """ - json = {} + assert self.bot is not None + json: Dict[str, Any] = {} if self.icon: dump = self.icon.model_dump() @@ -70,7 +73,8 @@ class EditChat(BaseConnection): if not None in counter or \ not counter[None] == 2: - return logger.error( + + raise MaxIconParamsException( 'Все атрибуты модели Icon являются взаимоисключающими | ' 'https://dev.max.ru/docs-api/methods/PATCH/chats/-chatId-' ) diff --git a/maxapi/methods/edit_message.py b/maxapi/methods/edit_message.py index 9cfbb3f..049b880 100644 --- a/maxapi/methods/edit_message.py +++ b/maxapi/methods/edit_message.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import List, TYPE_CHECKING, Optional +from typing import Any, Dict, List, TYPE_CHECKING, Optional + +from ..utils.message import process_input_media from .types.edited_message import EditedMessage from ..types.message import NewMessageLink @@ -37,10 +39,10 @@ class EditMessage(BaseConnection): self, bot: Bot, message_id: str, - text: str = None, - attachments: List[Attachment | InputMedia | InputMediaBuffer] = None, - link: NewMessageLink = None, - notify: bool = True, + text: Optional[str] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, + link: Optional[NewMessageLink] = None, + notify: Optional[bool] = None, parse_mode: Optional[ParseMode] = None ): self.bot = bot @@ -51,7 +53,7 @@ class EditMessage(BaseConnection): self.notify = notify self.parse_mode = parse_mode - async def request(self) -> EditedMessage: + async def fetch(self) -> EditedMessage: """ Выполняет PUT-запрос для обновления сообщения. @@ -62,15 +64,31 @@ class EditMessage(BaseConnection): EditedMessage: Обновлённое сообщение. """ + assert self.bot is not None params = self.bot.params.copy() - json = {} + json: Dict[str, Any] = {} params['message_id'] = self.message_id if not self.text is None: json['text'] = self.text - if self.attachments: json['attachments'] = \ - [att.model_dump() for att in self.attachments] + + if self.attachments: + + for att in self.attachments: + + if isinstance(att, InputMedia) or isinstance(att, InputMediaBuffer): + input_media = await process_input_media( + base_connection=self, + bot=self.bot, + att=att + ) + json['attachments'].append( + input_media.model_dump() + ) + else: + 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 if not self.parse_mode is None: json['format'] = self.parse_mode.value diff --git a/maxapi/methods/get_chat_by_id.py b/maxapi/methods/get_chat_by_id.py index 7b41c0a..b67b7b0 100644 --- a/maxapi/methods/get_chat_by_id.py +++ b/maxapi/methods/get_chat_by_id.py @@ -30,7 +30,7 @@ class GetChatById(BaseConnection): self.bot = bot self.id = id - async def request(self) -> Chat: + async def fetch(self) -> Chat: """ Выполняет GET-запрос для получения данных чата. @@ -39,6 +39,7 @@ class GetChatById(BaseConnection): Chat: Объект чата с полной информацией. """ + assert self.bot is not None return await super().request( method=HTTPMethod.GET, path=ApiPath.CHATS.value + '/' + str(self.id), diff --git a/maxapi/methods/get_chat_by_link.py b/maxapi/methods/get_chat_by_link.py index 264a575..b9d6f40 100644 --- a/maxapi/methods/get_chat_by_link.py +++ b/maxapi/methods/get_chat_by_link.py @@ -40,7 +40,7 @@ class GetChatByLink(BaseConnection): if not self.link: return - async def request(self) -> Chat: + async def fetch(self) -> Chat: """ Выполняет GET-запрос для получения данных чата по ссылке. @@ -49,6 +49,7 @@ class GetChatByLink(BaseConnection): Chat: Объект с информацией о чате. """ + assert self.bot is not None return await super().request( method=HTTPMethod.GET, path=ApiPath.CHATS.value + '/' + self.link[-1], diff --git a/maxapi/methods/get_chats.py b/maxapi/methods/get_chats.py index 84858e0..be2dacd 100644 --- a/maxapi/methods/get_chats.py +++ b/maxapi/methods/get_chats.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from ..types.chats import Chats @@ -32,13 +32,13 @@ class GetChats(BaseConnection): self, bot: 'Bot', count: int = 50, - marker: int = None + marker: Optional[int] = None ): self.bot = bot self.count = count self.marker = marker - async def request(self) -> Chats: + async def fetch(self) -> Chats: """ Выполняет GET-запрос для получения списка чатов. @@ -46,7 +46,7 @@ class GetChats(BaseConnection): Returns: Chats: Объект с данными по списку чатов. """ - + assert self.bot is not None params = self.bot.params.copy() params['count'] = self.count diff --git a/maxapi/methods/get_list_admin_chat.py b/maxapi/methods/get_list_admin_chat.py index ad2509f..38e0732 100644 --- a/maxapi/methods/get_list_admin_chat.py +++ b/maxapi/methods/get_list_admin_chat.py @@ -34,7 +34,7 @@ class GetListAdminChat(BaseConnection): self.bot = bot self.chat_id = chat_id - async def request(self) -> GettedListAdminChat: + async def fetch(self) -> GettedListAdminChat: """ Выполняет GET-запрос для получения списка администраторов указанного чата. @@ -42,7 +42,7 @@ class GetListAdminChat(BaseConnection): Returns: GettedListAdminChat: Объект с информацией о администраторах чата. """ - + assert self.bot is not None return await super().request( method=HTTPMethod.GET, path=ApiPath.CHATS.value + '/' + str(self.chat_id) + ApiPath.MEMBERS + ApiPath.ADMINS, diff --git a/maxapi/methods/get_me.py b/maxapi/methods/get_me.py index 87b75af..d113042 100644 --- a/maxapi/methods/get_me.py +++ b/maxapi/methods/get_me.py @@ -24,7 +24,7 @@ class GetMe(BaseConnection): def __init__(self, bot: 'Bot'): self.bot = bot - async def request(self) -> User: + async def fetch(self) -> User: """ Выполняет GET-запрос для получения данных о боте. @@ -32,7 +32,7 @@ class GetMe(BaseConnection): Returns: User: Объект пользователя с полной информацией. """ - + assert self.bot is not None return await super().request( method=HTTPMethod.GET, path=ApiPath.ME, diff --git a/maxapi/methods/get_me_from_chat.py b/maxapi/methods/get_me_from_chat.py index 7710c42..5b6a822 100644 --- a/maxapi/methods/get_me_from_chat.py +++ b/maxapi/methods/get_me_from_chat.py @@ -34,7 +34,7 @@ class GetMeFromChat(BaseConnection): self.bot = bot self.chat_id = chat_id - async def request(self) -> ChatMember: + async def fetch(self) -> ChatMember: """ Выполняет GET-запрос для получения информации о боте в указанном чате. @@ -42,7 +42,7 @@ class GetMeFromChat(BaseConnection): Returns: ChatMember: Информация о боте как участнике чата. """ - + assert self.bot is not None return await super().request( method=HTTPMethod.GET, path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.MEMBERS + ApiPath.ME, diff --git a/maxapi/methods/get_members_chat.py b/maxapi/methods/get_members_chat.py index ffe0207..d980da0 100644 --- a/maxapi/methods/get_members_chat.py +++ b/maxapi/methods/get_members_chat.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional from ..methods.types.getted_members_chat import GettedMembersChat @@ -27,7 +27,7 @@ class GetMembersChat(BaseConnection): Attributes: bot (Bot): Экземпляр бота. chat_id (int): Идентификатор чата. - user_ids (List[str] | None): Список ID пользователей для фильтра. + user_ids (List[int] | None): Список ID пользователей для фильтра. marker (int | None): Позиция для пагинации. count (int | None): Максимальное количество участников. """ @@ -36,9 +36,9 @@ class GetMembersChat(BaseConnection): self, bot: 'Bot', chat_id: int, - user_ids: List[str] = None, - marker: int = None, - count: int = None, + user_ids: Optional[List[int]] = None, + marker: Optional[int] = None, + count: Optional[int] = None, ): self.bot = bot @@ -47,7 +47,7 @@ class GetMembersChat(BaseConnection): self.marker = marker self.count = count - async def request(self) -> GettedMembersChat: + async def fetch(self) -> GettedMembersChat: """ Выполняет GET-запрос для получения участников чата с опциональной фильтрацией. @@ -57,12 +57,12 @@ class GetMembersChat(BaseConnection): Returns: GettedMembersChat: Объект с данными по участникам чата. """ - + assert self.bot is not None params = self.bot.params.copy() if self.user_ids: - self.user_ids = [str(user_id) for user_id in self.user_ids] - params['user_ids'] = ','.join(self.user_ids) + params['user_ids'] = ','.join([str(user_id) for user_id in self.user_ids]) + if self.marker: params['marker'] = self.marker if self.count: params['marker'] = self.count diff --git a/maxapi/methods/get_messages.py b/maxapi/methods/get_messages.py index 0c55db4..e30f814 100644 --- a/maxapi/methods/get_messages.py +++ b/maxapi/methods/get_messages.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional, Union from ..types.message import Messages from ..enums.http_method import HTTPMethod @@ -36,10 +36,10 @@ class GetMessages(BaseConnection): def __init__( self, bot: 'Bot', - chat_id: int, - message_ids: List[str] = None, - from_time: datetime | int = None, - to_time: datetime | int = None, + chat_id: Optional[int] = None, + message_ids: Optional[List[str]] = None, + from_time: Optional[Union[datetime, int]] = None, + to_time: Optional[Union[datetime, int]] = None, count: int = 50, ): self.bot = bot @@ -49,7 +49,7 @@ class GetMessages(BaseConnection): self.to_time = to_time self.count = count - async def request(self) -> Messages: + async def fetch(self) -> Messages: """ Выполняет GET-запрос для получения сообщений с учётом параметров фильтрации. @@ -59,7 +59,7 @@ class GetMessages(BaseConnection): Returns: Messages: Объект с полученными сообщениями. """ - + assert self.bot is not None params = self.bot.params.copy() if self.chat_id: params['chat_id'] = self.chat_id diff --git a/maxapi/methods/get_pinned_message.py b/maxapi/methods/get_pinned_message.py index bdc5f5e..fb4d0b3 100644 --- a/maxapi/methods/get_pinned_message.py +++ b/maxapi/methods/get_pinned_message.py @@ -29,7 +29,7 @@ class GetPinnedMessage(BaseConnection): self.bot = bot self.chat_id = chat_id - async def request(self) -> GettedPin: + async def fetch(self) -> GettedPin: """ Выполняет GET-запрос для получения закреплённого сообщения. @@ -37,7 +37,7 @@ class GetPinnedMessage(BaseConnection): Returns: GettedPin: Объект с информацией о закреплённом сообщении. """ - + assert self.bot is not None return await super().request( method=HTTPMethod.GET, path=ApiPath.CHATS + '/' + str(self.chat_id) + ApiPath.PIN, diff --git a/maxapi/methods/get_updates.py b/maxapi/methods/get_updates.py index 71ba546..a27b169 100644 --- a/maxapi/methods/get_updates.py +++ b/maxapi/methods/get_updates.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING +from __future__ import annotations +from typing import TYPE_CHECKING, Dict from ..types.updates import UpdateUnion @@ -28,13 +29,13 @@ class GetUpdates(BaseConnection): def __init__( self, - bot: 'Bot', + bot: Bot, limit: int = 100, ): self.bot = bot self.limit = limit - async def request(self) -> UpdateUnion: + async def fetch(self) -> Dict: """ Выполняет GET-запрос для получения обновлений с указанным лимитом. @@ -44,7 +45,7 @@ class GetUpdates(BaseConnection): Returns: UpdateUnion: Объединённый тип данных обновлений. """ - + assert self.bot is not None params = self.bot.params.copy() params['limit'] = self.limit diff --git a/maxapi/methods/get_upload_url.py b/maxapi/methods/get_upload_url.py index 674bb0b..423c35a 100644 --- a/maxapi/methods/get_upload_url.py +++ b/maxapi/methods/get_upload_url.py @@ -33,7 +33,7 @@ class GetUploadURL(BaseConnection): self.bot = bot self.type = type - async def request(self) -> GettedUploadUrl: + async def fetch(self) -> GettedUploadUrl: """ Выполняет POST-запрос для получения URL загрузки файла. @@ -43,7 +43,7 @@ class GetUploadURL(BaseConnection): Returns: GettedUploadUrl: Результат с URL для загрузки. """ - + assert self.bot is not None params = self.bot.params.copy() params['type'] = self.type.value diff --git a/maxapi/methods/get_video.py b/maxapi/methods/get_video.py index 04d2aef..ab10cd1 100644 --- a/maxapi/methods/get_video.py +++ b/maxapi/methods/get_video.py @@ -30,7 +30,7 @@ class GetVideo(BaseConnection): self.bot = bot self.video_token = video_token - async def request(self) -> Video: + async def fetch(self) -> Video: """ Выполняет GET-запрос для получения данных видео по токену. @@ -38,7 +38,7 @@ class GetVideo(BaseConnection): Returns: Video: Объект с информацией о видео. """ - + assert self.bot is not None return await super().request( method=HTTPMethod.GET, path=ApiPath.VIDEOS.value + '/' + self.video_token, diff --git a/maxapi/methods/pin_message.py b/maxapi/methods/pin_message.py index 60d82f7..feb7551 100644 --- a/maxapi/methods/pin_message.py +++ b/maxapi/methods/pin_message.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Optional from .types.pinned_message import PinnedMessage @@ -35,14 +35,14 @@ class PinMessage(BaseConnection): bot: 'Bot', chat_id: int, message_id: str, - notify: bool = True + notify: Optional[bool] = None ): self.bot = bot self.chat_id = chat_id self.message_id = message_id self.notify = notify - async def request(self) -> PinnedMessage: + async def fetch(self) -> PinnedMessage: """ Выполняет PUT-запрос для закрепления сообщения в чате. @@ -52,8 +52,8 @@ class PinMessage(BaseConnection): Returns: PinnedMessage: Объект с информацией о закреплённом сообщении. """ - - json = {} + assert self.bot is not None + json: Dict[str, Any] = {} json['message_id'] = self.message_id json['notify'] = self.notify diff --git a/maxapi/methods/remove_admin.py b/maxapi/methods/remove_admin.py index b180cd9..fad99a7 100644 --- a/maxapi/methods/remove_admin.py +++ b/maxapi/methods/remove_admin.py @@ -38,7 +38,7 @@ class RemoveAdmin(BaseConnection): self.chat_id = chat_id self.user_id = user_id - async def request(self) -> RemovedAdmin: + async def fetch(self) -> RemovedAdmin: """ Выполняет DELETE-запрос для отмены прав администратора в чате. @@ -46,11 +46,11 @@ class RemoveAdmin(BaseConnection): Returns: RemovedAdmin: Объект с результатом отмены прав администратора. """ - + assert self.bot is not None return await super().request( method=HTTPMethod.DELETE, path=ApiPath.CHATS + '/' + str(self.chat_id) + \ ApiPath.MEMBERS + ApiPath.ADMINS + '/' + str(self.user_id), model=RemovedAdmin, params=self.bot.params, - ) \ No newline at end of file + ) \ No newline at end of file diff --git a/maxapi/methods/remove_member_chat.py b/maxapi/methods/remove_member_chat.py index 48b00c0..bb567cf 100644 --- a/maxapi/methods/remove_member_chat.py +++ b/maxapi/methods/remove_member_chat.py @@ -43,7 +43,7 @@ class RemoveMemberChat(BaseConnection): self.user_id = user_id self.block = block - async def request(self) -> RemovedMemberChat: + async def fetch(self) -> RemovedMemberChat: """ Выполняет DELETE-запрос для удаления пользователя из чата. @@ -54,6 +54,7 @@ class RemoveMemberChat(BaseConnection): RemovedMemberChat: Результат удаления участника. """ + assert self.bot is not None params = self.bot.params.copy() params['chat_id'] = self.chat_id diff --git a/maxapi/methods/send_action.py b/maxapi/methods/send_action.py index 04718b5..9397de5 100644 --- a/maxapi/methods/send_action.py +++ b/maxapi/methods/send_action.py @@ -1,6 +1,6 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Optional from ..methods.types.sended_action import SendedAction @@ -34,14 +34,14 @@ class SendAction(BaseConnection): def __init__( self, bot: 'Bot', - chat_id: int = None, + chat_id: Optional[int] = None, action: SenderAction = SenderAction.TYPING_ON ): self.bot = bot self.chat_id = chat_id self.action = action - async def request(self) -> SendedAction: + async def fetch(self) -> SendedAction: """ Выполняет POST-запрос для отправки действия в указанный чат. @@ -49,8 +49,9 @@ class SendAction(BaseConnection): Returns: SendedAction: Результат выполнения запроса. """ + assert self.bot is not None - json = {} + json: Dict[str, Any] = {} json['action'] = self.action.value diff --git a/maxapi/methods/send_callback.py b/maxapi/methods/send_callback.py index 4355247..9e3f657 100644 --- a/maxapi/methods/send_callback.py +++ b/maxapi/methods/send_callback.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, Optional from ..methods.types.sended_callback import SendedCallback @@ -36,15 +36,15 @@ class SendCallback(BaseConnection): self, bot: 'Bot', callback_id: str, - message: Message = None, - notification: str = None + message: Optional[Message] = None, + notification: Optional[str] = None ): self.bot = bot self.callback_id = callback_id self.message = message self.notification = notification - async def request(self) -> SendedCallback: + async def fetch(self) -> SendedCallback: """ Выполняет POST-запрос для отправки callback-ответа. @@ -55,11 +55,12 @@ class SendCallback(BaseConnection): SendedCallback: Объект с результатом отправки callback. """ + assert self.bot is not None params = self.bot.params.copy() params['callback_id'] = self.callback_id - json = {} + json: Dict[str, Any] = {} if self.message: json['message'] = self.message.model_dump() if self.notification: json['notification'] = self.notification diff --git a/maxapi/methods/send_message.py b/maxapi/methods/send_message.py index 8d3333b..ab39b7e 100644 --- a/maxapi/methods/send_message.py +++ b/maxapi/methods/send_message.py @@ -1,10 +1,14 @@ import asyncio -from typing import List, TYPE_CHECKING, Optional +from typing import Any, Dict, List, TYPE_CHECKING, Optional from json import loads as json_loads +from ..utils.message import process_input_media + +from ..exceptions.max import MaxUploadFileFailed + from .types.sended_message import SendedMessage from ..types.attachments.upload import AttachmentPayload, AttachmentUpload from ..types.errors import Error @@ -39,7 +43,7 @@ class SendMessage(BaseConnection): chat_id (int, optional): Идентификатор чата, куда отправлять сообщение. user_id (int, optional): Идентификатор пользователя, если нужно отправить личное сообщение. text (str, optional): Текст сообщения. - attachments (List[Attachment | InputMedia], optional): Список вложений к сообщению. + attachments (List[Attachment | InputMedia | InputMediaBuffer], optional): Список вложений к сообщению. link (NewMessageLink, optional): Связь с другим сообщением (например, ответ или пересылка). notify (bool, optional): Отправлять ли уведомление о сообщении. По умолчанию True. parse_mode (ParseMode, optional): Режим разбора текста (например, Markdown, HTML). @@ -48,12 +52,12 @@ class SendMessage(BaseConnection): def __init__( self, bot: 'Bot', - chat_id: int = None, - user_id: int = None, - text: str = None, - attachments: List[Attachment | InputMedia] = None, - link: NewMessageLink = None, - notify: bool = True, + chat_id: Optional[int] = None, + user_id: Optional[int] = None, + text: Optional[str] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, + link: Optional[NewMessageLink] = None, + notify: Optional[bool] = None, parse_mode: Optional[ParseMode] = None ): self.bot = bot @@ -65,59 +69,7 @@ class SendMessage(BaseConnection): self.notify = notify self.parse_mode = parse_mode - async def __process_input_media( - self, - att: InputMedia | InputMediaBuffer - ): - - # очень нестабильный метод независящий от модуля - # ждем обновлений MAX API - - """ - Загружает файл вложения и формирует объект AttachmentUpload. - - Args: - att (InputMedia): Объект вложения для загрузки. - - Returns: - AttachmentUpload: Загруженное вложение с токеном. - """ - - upload = await self.bot.get_upload_url(att.type) - - if isinstance(att, InputMedia): - upload_file_response = await self.upload_file( - url=upload.url, - path=att.path, - type=att.type, - ) - elif isinstance(att, InputMediaBuffer): - upload_file_response = await self.upload_file_buffer( - url=upload.url, - buffer=att.buffer, - type=att.type, - ) - - if att.type in (UploadType.VIDEO, UploadType.AUDIO): - token = upload.token - - elif att.type == UploadType.FILE: - json_r = json_loads(upload_file_response) - token = json_r['token'] - - elif att.type == UploadType.IMAGE: - json_r = json_loads(upload_file_response) - json_r_keys = list(json_r['photos'].keys()) - token = json_r['photos'][json_r_keys[0]]['token'] - - return AttachmentUpload( - type=att.type, - payload=AttachmentPayload( - token=token - ) - ) - - async def request(self) -> SendedMessage: + async def fetch(self) -> Optional[SendedMessage | Error]: """ Отправляет сообщение с вложениями (если есть), с обработкой задержки готовности вложений. @@ -128,9 +80,10 @@ class SendMessage(BaseConnection): SendedMessage или Error """ + assert self.bot is not None params = self.bot.params.copy() - json = {'attachments': []} + json: Dict[str, Any] = {'attachments': []} if self.chat_id: params['chat_id'] = self.chat_id elif self.user_id: params['user_id'] = self.user_id @@ -142,7 +95,11 @@ class SendMessage(BaseConnection): for att in self.attachments: if isinstance(att, InputMedia) or isinstance(att, InputMediaBuffer): - input_media = await self.__process_input_media(att) + input_media = await process_input_media( + base_connection=self, + bot=self.bot, + att=att + ) json['attachments'].append( input_media.model_dump() ) diff --git a/maxapi/methods/types/getted_upload_url.py b/maxapi/methods/types/getted_upload_url.py index 13d7a20..c8ee254 100644 --- a/maxapi/methods/types/getted_upload_url.py +++ b/maxapi/methods/types/getted_upload_url.py @@ -3,5 +3,5 @@ from pydantic import BaseModel class GettedUploadUrl(BaseModel): - url: Optional[str] = None + url: str token: Optional[str] = None \ No newline at end of file diff --git a/maxapi/types/__init__.py b/maxapi/types/__init__.py index 18f18b9..dcfe635 100644 --- a/maxapi/types/__init__.py +++ b/maxapi/types/__init__.py @@ -32,35 +32,35 @@ from .input_media import InputMedia from .input_media import InputMediaBuffer __all__ = [ - CommandStart, - OpenAppButton, - Message, - Attachment, - InputMediaBuffer, - MessageButton, - UpdateUnion, - InputMedia, - BotCommand, - CallbackButton, - ChatButton, - LinkButton, - RequestContactButton, - RequestGeoLocationButton, - Command, - PhotoAttachmentPayload, - OtherAttachmentPayload, - ContactAttachmentPayload, - ButtonsPayload, - StickerAttachmentPayload, - BotAdded, - BotRemoved, - BotStarted, - ChatTitleChanged, - MessageCallback, - MessageChatCreated, - MessageCreated, - MessageEdited, - MessageRemoved, - UserAdded, - UserRemoved -] \ No newline at end of file + 'CommandStart', + 'OpenAppButton', + 'Message', + 'Attachment', + 'InputMediaBuffer', + 'MessageButton', + 'UpdateUnion', + 'InputMedia', + 'BotCommand', + 'CallbackButton', + 'ChatButton', + 'LinkButton', + 'RequestContactButton', + 'RequestGeoLocationButton', + 'Command', + 'PhotoAttachmentPayload', + 'OtherAttachmentPayload', + 'ContactAttachmentPayload', + 'ButtonsPayload', + 'StickerAttachmentPayload', + 'BotAdded', + 'BotRemoved', + 'BotStarted', + 'ChatTitleChanged', + 'MessageCallback', + 'MessageChatCreated', + 'MessageCreated', + 'MessageEdited', + 'MessageRemoved', + 'UserAdded', + 'UserRemoved', +] diff --git a/maxapi/types/attachments/attachment.py b/maxapi/types/attachments/attachment.py index fb1901a..20f9d97 100644 --- a/maxapi/types/attachments/attachment.py +++ b/maxapi/types/attachments/attachment.py @@ -112,33 +112,33 @@ class Attachment(BaseModel): bot: Optional[Any] = Field(default=None, exclude=True) if TYPE_CHECKING: - bot: Optional[Bot] + bot: Optional[Bot] # type: ignore class Config: use_enum_values = True - async def download( - self, - path: str - ): + # async def download( + # self, + # path: str + # ): - """ - Скачивает медиа, сохраняя по определенному пути + # """ + # Скачивает медиа, сохраняя по определенному пути - :param path: Путь сохранения медиа + # :param path: Путь сохранения медиа - :return: Числовой статус - """ + # :return: Числовой статус + # """ - if not hasattr(self.payload, 'token') or \ - not hasattr(self.payload, 'url'): - raise NotAvailableForDownload() + # if not hasattr(self.payload, 'token') or \ + # not hasattr(self.payload, 'url'): + # raise NotAvailableForDownload() - elif not self.payload.token or not self.payload.url: - raise NotAvailableForDownload(f'Медиа типа `{self.type}` недоступно для скачивания') + # elif not self.payload.token or not self.payload.url: + # raise NotAvailableForDownload(f'Медиа типа `{self.type}` недоступно для скачивания') - return await self.bot.download_file( - path=path, - url=self.payload.url, - token=self.payload.token, - ) \ No newline at end of file + # return await self.bot.download_file( + # path=path, + # url=self.payload.url, + # token=self.payload.token, + # ) \ No newline at end of file diff --git a/maxapi/types/attachments/audio.py b/maxapi/types/attachments/audio.py index b981bcd..f527456 100644 --- a/maxapi/types/attachments/audio.py +++ b/maxapi/types/attachments/audio.py @@ -1,4 +1,6 @@ -from typing import Literal, Optional +from typing import Optional + +from ...enums.attachment import AttachmentType from .attachment import Attachment @@ -13,5 +15,5 @@ class Audio(Attachment): transcription (Optional[str]): Транскрипция аудио (если есть). """ - type: Literal['audio'] = 'audio' + type: AttachmentType = AttachmentType.AUDIO transcription: Optional[str] = None \ No newline at end of file diff --git a/maxapi/types/attachments/contact.py b/maxapi/types/attachments/contact.py index 18e74f1..177f969 100644 --- a/maxapi/types/attachments/contact.py +++ b/maxapi/types/attachments/contact.py @@ -1,4 +1,4 @@ -from typing import Literal +from ...enums.attachment import AttachmentType from .attachment import Attachment @@ -12,4 +12,4 @@ class Contact(Attachment): type (Literal['contact']): Тип вложения, всегда 'contact'. """ - type: Literal['contact'] = 'contact' \ No newline at end of file + type: AttachmentType = AttachmentType.CONTACT \ No newline at end of file diff --git a/maxapi/types/attachments/file.py b/maxapi/types/attachments/file.py index 02a7d0b..3a3801a 100644 --- a/maxapi/types/attachments/file.py +++ b/maxapi/types/attachments/file.py @@ -1,4 +1,6 @@ -from typing import Literal, Optional +from typing import Optional + +from ...enums.attachment import AttachmentType from .attachment import Attachment @@ -14,6 +16,6 @@ class File(Attachment): size (Optional[int]): Размер файла в байтах. """ - type: Literal['file'] = 'file' + type: AttachmentType = AttachmentType.FILE filename: Optional[str] = None size: Optional[int] = None \ No newline at end of file diff --git a/maxapi/types/attachments/image.py b/maxapi/types/attachments/image.py index acfdcb1..ac94c4a 100644 --- a/maxapi/types/attachments/image.py +++ b/maxapi/types/attachments/image.py @@ -1,7 +1,9 @@ -from typing import Literal, Optional +from typing import Optional from pydantic import BaseModel + from .attachment import Attachment +from ...enums.attachment import AttachmentType class PhotoAttachmentRequestPayload(BaseModel): @@ -29,4 +31,4 @@ class Image(Attachment): type (Literal['image']): Тип вложения, всегда 'image'. """ - type: Literal['image'] = 'image' \ No newline at end of file + type: AttachmentType = AttachmentType.IMAGE \ No newline at end of file diff --git a/maxapi/types/attachments/location.py b/maxapi/types/attachments/location.py index b501ef4..d161ced 100644 --- a/maxapi/types/attachments/location.py +++ b/maxapi/types/attachments/location.py @@ -1,4 +1,6 @@ -from typing import Literal, Optional +from typing import Optional + +from ...enums.attachment import AttachmentType from .attachment import Attachment @@ -14,6 +16,6 @@ class Location(Attachment): longitude (Optional[float]): Долгота. """ - type: Literal['location'] = 'location' + type: AttachmentType = AttachmentType.LOCATION latitude: Optional[float] = None longitude: Optional[float] = None \ No newline at end of file diff --git a/maxapi/types/attachments/share.py b/maxapi/types/attachments/share.py index 4f6e19c..8ca9bfa 100644 --- a/maxapi/types/attachments/share.py +++ b/maxapi/types/attachments/share.py @@ -1,4 +1,6 @@ -from typing import Literal, Optional +from typing import Optional + +from ...enums.attachment import AttachmentType from .attachment import Attachment @@ -15,7 +17,7 @@ class Share(Attachment): image_url (Optional[str]): URL изображения для предпросмотра. """ - type: Literal['share'] = 'share' + type: AttachmentType = AttachmentType.SHARE title: Optional[str] = None description: Optional[str] = None image_url: Optional[str] = None diff --git a/maxapi/types/attachments/sticker.py b/maxapi/types/attachments/sticker.py index a2278de..5cdebd7 100644 --- a/maxapi/types/attachments/sticker.py +++ b/maxapi/types/attachments/sticker.py @@ -1,4 +1,6 @@ -from typing import Literal, Optional +from typing import Optional + +from ...enums.attachment import AttachmentType from .attachment import Attachment @@ -14,6 +16,6 @@ class Sticker(Attachment): height (Optional[int]): Высота стикера в пикселях. """ - type: Literal['sticker'] = 'sticker' + type: AttachmentType = AttachmentType.STICKER width: Optional[int] = None height: Optional[int] = None \ No newline at end of file diff --git a/maxapi/types/attachments/video.py b/maxapi/types/attachments/video.py index 6a19508..c72bd05 100644 --- a/maxapi/types/attachments/video.py +++ b/maxapi/types/attachments/video.py @@ -1,6 +1,8 @@ from typing import TYPE_CHECKING, Any, Literal, Optional from pydantic import BaseModel, Field +from ...enums.attachment import AttachmentType + from .attachment import Attachment if TYPE_CHECKING: @@ -59,7 +61,7 @@ class Video(Attachment): bot (Optional[Any]): Ссылка на экземпляр бота, не сериализуется. """ - type: Optional[Literal['video']] = 'video' + type: AttachmentType = AttachmentType.VIDEO token: Optional[str] = None urls: Optional[VideoUrl] = None thumbnail: VideoThumbnail @@ -69,4 +71,4 @@ class Video(Attachment): bot: Optional[Any] = Field(default=None, exclude=True) if TYPE_CHECKING: - bot: Optional['Bot'] + bot: Optional['Bot'] # type: ignore diff --git a/maxapi/types/input_media.py b/maxapi/types/input_media.py index 8e30080..3e2e539 100644 --- a/maxapi/types/input_media.py +++ b/maxapi/types/input_media.py @@ -70,11 +70,11 @@ class InputMediaBuffer: Класс для представления медиафайла из буфера. Attributes: - buffer (BytesIO): Буфер с содержимым файла. + buffer (bytes): Буфер с содержимым файла. type (UploadType): Тип файла, определенный по содержимому. """ - def __init__(self, buffer: BytesIO): + def __init__(self, buffer: bytes): """ Инициализирует объект медиафайла из буфера. @@ -84,7 +84,7 @@ class InputMediaBuffer: self.buffer = buffer self.type = self.__detect_file_type(buffer) - def __detect_file_type(self, buffer: BytesIO) -> UploadType: + def __detect_file_type(self, buffer: bytes) -> UploadType: try: matches = puremagic.magic_string(buffer) if matches: diff --git a/maxapi/types/message.py b/maxapi/types/message.py index 1db7513..9091e7d 100644 --- a/maxapi/types/message.py +++ b/maxapi/types/message.py @@ -89,7 +89,7 @@ class MessageBody(BaseModel): mid: str seq: int - text: str = None + text: Optional[str] = None attachments: Optional[ List[ Union[ @@ -103,7 +103,7 @@ class MessageBody(BaseModel): Location ] ] - ] = Field(default_factory=list) + ] = Field(default_factory=list) # type: ignore markup: Optional[ List[ @@ -111,7 +111,7 @@ class MessageBody(BaseModel): MarkupLink, MarkupElement ] ] - ] = Field(default_factory=list) + ] = Field(default_factory=list) # type: ignore class MessageStat(BaseModel): @@ -164,19 +164,19 @@ class Message(BaseModel): recipient: Recipient timestamp: int link: Optional[LinkedMessage] = None - body: Optional[MessageBody] = None + body: MessageBody stat: Optional[MessageStat] = None url: Optional[str] = None bot: Optional[Any] = Field(default=None, exclude=True) if TYPE_CHECKING: - bot: Optional[Bot] + bot: Optional[Bot] # type: ignore async def answer( self, - text: str = None, - attachments: List[Attachment | InputMedia | InputMediaBuffer] = None, - link: NewMessageLink = None, + text: Optional[str] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, + link: Optional[NewMessageLink] = None, notify: Optional[bool] = None, parse_mode: Optional[ParseMode] = None ): @@ -195,6 +195,7 @@ class Message(BaseModel): Any: Результат выполнения метода send_message бота. """ + assert self.bot is not None return await self.bot.send_message( chat_id=self.recipient.chat_id, user_id=self.recipient.user_id, @@ -207,8 +208,8 @@ class Message(BaseModel): async def reply( self, - text: str = None, - attachments: List[Attachment | InputMedia | InputMediaBuffer] = None, + text: Optional[str] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, notify: Optional[bool] = None, parse_mode: Optional[ParseMode] = None ): @@ -226,6 +227,7 @@ class Message(BaseModel): Any: Результат выполнения метода send_message бота. """ + assert self.bot is not None return await self.bot.send_message( chat_id=self.recipient.chat_id, user_id=self.recipient.user_id, @@ -242,8 +244,8 @@ class Message(BaseModel): async def forward( self, chat_id, - user_id: int = None, - attachments: List[Attachment | InputMedia | InputMediaBuffer] = None, + user_id: Optional[int] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, notify: Optional[bool] = None, parse_mode: Optional[ParseMode] = None ): @@ -262,6 +264,7 @@ class Message(BaseModel): Any: Результат выполнения метода send_message бота. """ + assert self.bot is not None return await self.bot.send_message( chat_id=chat_id, user_id=user_id, @@ -276,9 +279,9 @@ class Message(BaseModel): async def edit( self, - text: str = None, - attachments: List[Attachment | InputMedia | InputMediaBuffer] = None, - link: NewMessageLink = None, + text: Optional[str] = None, + attachments: Optional[List[Attachment | InputMedia | InputMediaBuffer]] = None, + link: Optional[NewMessageLink] = None, notify: bool = True, parse_mode: Optional[ParseMode] = None ): @@ -297,6 +300,7 @@ class Message(BaseModel): Any: Результат выполнения метода edit_message бота. """ + assert self.bot is not None return await self.bot.edit_message( message_id=self.body.mid, text=text, @@ -331,6 +335,7 @@ class Message(BaseModel): Any: Результат выполнения метода pin_message бота. """ + assert self.bot is not None return await self.bot.pin_message( chat_id=self.recipient.chat_id, message_id=self.body.mid, @@ -352,7 +357,7 @@ class Messages(BaseModel): bot: Optional[Any] = Field(default=None, exclude=True) if TYPE_CHECKING: - bot: Optional[Bot] + bot: Optional[Bot] # type: ignore class NewMessageLink(BaseModel): diff --git a/maxapi/types/updates/message_callback.py b/maxapi/types/updates/message_callback.py index 4448f45..6d990ef 100644 --- a/maxapi/types/updates/message_callback.py +++ b/maxapi/types/updates/message_callback.py @@ -2,6 +2,8 @@ from typing import List, Optional, TYPE_CHECKING, Union from pydantic import BaseModel, Field +from ...types.attachments.location import Location + from .update import Update from ...enums.parse_mode import ParseMode @@ -49,10 +51,11 @@ class MessageForCallback(BaseModel): File, Image, Sticker, - Share + Share, + Location ] ] - ] = Field(default_factory=list) + ] = Field(default_factory=list) # type: ignore link: Optional[NewMessageLink] = None notify: Optional[bool] = True format: Optional[ParseMode] = None @@ -86,9 +89,9 @@ class MessageCallback(Update): async def answer( self, - notification: str = None, - new_text: str = None, - link: NewMessageLink = None, + notification: Optional[str] = None, + new_text: Optional[str] = None, + link: Optional[NewMessageLink] = None, notify: bool = True, format: Optional[ParseMode] = None, ): @@ -115,6 +118,7 @@ class MessageCallback(Update): message.notify = notify message.format = format + assert self.bot is not None return await self.bot.send_callback( callback_id=self.callback.callback_id, message=message, diff --git a/maxapi/utils/message.py b/maxapi/utils/message.py new file mode 100644 index 0000000..37ea9e6 --- /dev/null +++ b/maxapi/utils/message.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from json import loads + +from ..types.input_media import InputMedia, InputMediaBuffer +from ..enums.upload_type import UploadType +from ..exceptions.max import MaxUploadFileFailed +from ..types.attachments.upload import AttachmentPayload, AttachmentUpload + +if TYPE_CHECKING: + from ..bot import Bot + from ..connection.base import BaseConnection + + +async def process_input_media( + base_connection: BaseConnection, + bot: Bot, + att: InputMedia | InputMediaBuffer + ): + + # очень нестабильный метод независящий от модуля + # ждем обновлений MAX API + + """ + Загружает файл вложения и формирует объект AttachmentUpload. + + Args: + att (InputMedia): Объект вложения для загрузки. + + Returns: + AttachmentUpload: Загруженное вложение с токеном. + """ + + upload = await bot.get_upload_url(att.type) + + if isinstance(att, InputMedia): + upload_file_response = await base_connection.upload_file( + url=upload.url, + path=att.path, + type=att.type, + ) + elif isinstance(att, InputMediaBuffer): + upload_file_response = await base_connection.upload_file_buffer( + url=upload.url, + buffer=att.buffer, + type=att.type, + ) + + if att.type in (UploadType.VIDEO, UploadType.AUDIO): + if upload.token is None: + assert bot.session is not None + await bot.session.close() + raise MaxUploadFileFailed('По неизвестной причине token не был получен') + + token = upload.token + + elif att.type == UploadType.FILE: + json_r = loads(upload_file_response) + token = json_r['token'] + + elif att.type == UploadType.IMAGE: + json_r = loads(upload_file_response) + json_r_keys = list(json_r['photos'].keys()) + token = json_r['photos'][json_r_keys[0]]['token'] + + return AttachmentUpload( + type=att.type, + payload=AttachmentPayload( + token=token + ) + ) \ No newline at end of file diff --git a/wiki/memory_context.md b/wiki/memory_context.md index 6527786..466af25 100644 --- a/wiki/memory_context.md +++ b/wiki/memory_context.md @@ -46,7 +46,7 @@ MemoryContext(chat_id: int, user_id: int) --- -### `async def set_state(state: State | str = None)` +### `async def set_state(state: Optional[Union[State, str]] = None)` Устанавливает новое состояние пользователя или сбрасывает его.