From 1c6cfd0591e8df597c752fe941e1980d8f4d63d2 Mon Sep 17 00:00:00 2001 From: loveapples Date: Fri, 10 Jul 2026 23:45:09 +0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D1=83?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D0=BD=D0=B8=D0=BA=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D1=87=D0=B0=D1=82=D0=B0,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=D0=B1?= =?UTF-8?q?=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B8=D1=85?= =?UTF-8?q?=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- maxapi/bot.py | 31 +++++++-- maxapi/enums/chat_permission.py | 7 +- maxapi/methods/add_admin_chat.py | 22 +++++-- maxapi/methods/add_members_chat.py | 4 +- maxapi/methods/get_chat_by_link.py | 13 ++++ maxapi/methods/types/added_members_chat.py | 4 +- maxapi/types/chats.py | 17 ++++- tests/test_highlevel_shortcuts.py | 2 +- tests/test_swagger_alignment.py | 74 +++++++++++++++++++++- 9 files changed, 147 insertions(+), 27 deletions(-) diff --git a/maxapi/bot.py b/maxapi/bot.py index f0642008..ccdac9dd 100644 --- a/maxapi/bot.py +++ b/maxapi/bot.py @@ -692,6 +692,10 @@ async def get_chat_by_link(self, link: str) -> Chat: """ Получает канал по публичной ссылке или алиасу. + .. deprecated:: 1.2.1 + Метод удалён из текущей OpenAPI-спецификации API MAX. + Использование не рекомендуется. + https://dev.max.ru/docs-api/methods/GET/chats/-chatLink- Args: @@ -701,6 +705,14 @@ async def get_chat_by_link(self, link: str) -> Chat: Chat: Объект чата. """ + warnings.warn( + "bot.get_chat_by_link() устарел и отсутствует в текущей " + "OpenAPI-спецификации API MAX. " + "Использование не рекомендуется.", + DeprecationWarning, + stacklevel=2, + ) + return await GetChatByLink(bot=self, link=link).fetch() async def get_chat_by_id(self, id: int) -> Chat: @@ -913,25 +925,32 @@ async def add_list_admin_chat( marker: int | None = None, ) -> AddedListAdminChat: """ - Добавляет администраторов в чат. + Назначает администраторов чата или канала. https://dev.max.ru/docs-api/methods/POST/chats/-chatId-/members/admins Args: chat_id: ID чата. admins: Список администраторов. - marker: Указатель на следующую страницу данных. - По умолчанию None. + marker: Устаревший параметр, больше не отправляется в API. Returns: AddedListAdminChat: Результат добавления. """ + if marker is not None: + warnings.warn( + "Параметр marker в bot.add_list_admin_chat() устарел " + "и игнорируется: POST /chats/{chatId}/members/admins " + "больше не поддерживает marker.", + DeprecationWarning, + stacklevel=2, + ) + return await AddAdminChat( bot=self, chat_id=chat_id, admins=admins, - marker=marker, ).fetch() async def remove_admin(self, chat_id: int, user_id: int) -> RemovedAdmin: @@ -1021,12 +1040,12 @@ async def add_chat_members( user_ids: list[int], ) -> AddedMembersChat: """ - Добавляет участников в чат. + Добавляет участников в групповой чат. https://dev.max.ru/docs-api/methods/POST/chats/-chatId-/members Args: - chat_id: ID чата. + chat_id: ID группового чата. user_ids: Список ID пользователей. Returns: diff --git a/maxapi/enums/chat_permission.py b/maxapi/enums/chat_permission.py index fe6302dc..071eff6c 100644 --- a/maxapi/enums/chat_permission.py +++ b/maxapi/enums/chat_permission.py @@ -6,10 +6,11 @@ @unique class ChatPermission(StrEnum): """ - Права доступа пользователя в чате. + Права доступа пользователя или бота в чате или канале. - Используются для управления разрешениями при добавлении участников - или изменении настроек чата. + `add_remove_members` позволяет ботам добавлять и удалять участников + только в групповых чатах. Добавление подписчиков в канал через + `POST /chats/{chatId}/members` не поддерживается. """ READ_ALL_MESSAGES = auto() diff --git a/maxapi/methods/add_admin_chat.py b/maxapi/methods/add_admin_chat.py index 7635f844..bcf51baf 100644 --- a/maxapi/methods/add_admin_chat.py +++ b/maxapi/methods/add_admin_chat.py @@ -1,3 +1,4 @@ +import warnings from typing import TYPE_CHECKING, Any, cast from ..connection.base import BaseConnection @@ -12,16 +13,15 @@ class AddAdminChat(BaseConnection): """ - Класс для добавления списка администраторов в чат через API. + Класс для назначения администраторов чата или канала через API. https://dev.max.ru/docs-api/methods/POST/chats/-chatId-/members/admins Attributes: bot: Экземпляр бота, через который выполняется запрос. - chat_id: Идентификатор чата. - admins: Список администраторов для добавления. - marker: Маркер для пагинации или дополнительных - настроек. По умолчанию None. + chat_id: Идентификатор чата или канала. + admins: Список администраторов для назначения. + marker: Устаревший параметр, больше не отправляется в API. """ def __init__( @@ -31,6 +31,15 @@ def __init__( admins: list[ChatAdmin], marker: int | None = None, ): + if marker is not None: + warnings.warn( + "Параметр marker в AddAdminChat устарел и " + "игнорируется: POST /chats/{chatId}/members/admins " + "больше не поддерживает marker.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__() self.bot = bot self.chat_id = chat_id @@ -39,7 +48,7 @@ def __init__( async def fetch(self) -> AddedListAdminChat: """ - Выполняет HTTP POST запрос для добавления администраторов в чат. + Выполняет HTTP POST запрос для назначения администраторов. Формирует JSON с данными администраторов и отправляет запрос на соответствующий API-эндпоинт. @@ -54,7 +63,6 @@ async def fetch(self) -> AddedListAdminChat: json: dict[str, Any] = {} json["admins"] = [admin.model_dump() for admin in self.admins] - json["marker"] = self.marker response = await super().request( method=HTTPMethod.POST, diff --git a/maxapi/methods/add_members_chat.py b/maxapi/methods/add_members_chat.py index ad658c02..85cc996c 100644 --- a/maxapi/methods/add_members_chat.py +++ b/maxapi/methods/add_members_chat.py @@ -11,13 +11,13 @@ class AddMembersChat(BaseConnection): """ - Класс для добавления участников в чат через API. + Класс для добавления участников в групповой чат через API. https://dev.max.ru/docs-api/methods/POST/chats/-chatId-/members Attributes: bot: Экземпляр бота, через который выполняется запрос. - chat_id: Идентификатор целевого чата. + chat_id: Идентификатор группового чата. user_ids: Список ID пользователей для добавления в чат. """ diff --git a/maxapi/methods/get_chat_by_link.py b/maxapi/methods/get_chat_by_link.py index d33bf2b9..c7146350 100644 --- a/maxapi/methods/get_chat_by_link.py +++ b/maxapi/methods/get_chat_by_link.py @@ -1,3 +1,4 @@ +import warnings from re import fullmatch from typing import TYPE_CHECKING, cast from urllib.parse import urlparse @@ -15,6 +16,10 @@ class GetChatByLink(BaseConnection): """ Класс для получения информации о канале по публичной ссылке. + .. deprecated:: 1.2.1 + Метод удалён из текущей OpenAPI-спецификации API MAX. + Использование не рекомендуется. + https://dev.max.ru/docs-api/methods/GET/chats/-chatLink- Attributes: @@ -25,6 +30,14 @@ class GetChatByLink(BaseConnection): PATTERN_LINK: str = r"@?[a-zA-Z]+[a-zA-Z0-9-_]*" def __init__(self, bot: "Bot", link: str): + warnings.warn( + "GetChatByLink устарел и отсутствует в текущей " + "OpenAPI-спецификации API MAX. " + "Использование не рекомендуется.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__() self.bot = bot self.link = self._normalize_link(link) diff --git a/maxapi/methods/types/added_members_chat.py b/maxapi/methods/types/added_members_chat.py index d48da45b..21ba9bd4 100644 --- a/maxapi/methods/types/added_members_chat.py +++ b/maxapi/methods/types/added_members_chat.py @@ -7,7 +7,7 @@ class FailedUserDetails(BaseModel): """ - Детали ошибки для пользователя. + Детали ошибки добавления пользователя в групповой чат. Attributes: error_code: Код ошибки. @@ -21,7 +21,7 @@ class FailedUserDetails(BaseModel): class AddedMembersChat(BaseModel): """ - Ответ API при добавлении списка пользователей в чат. + Ответ API при добавлении списка пользователей в групповой чат. Attributes: success: Статус успешности операции. diff --git a/maxapi/types/chats.py b/maxapi/types/chats.py index 1776b578..6bc18cc2 100644 --- a/maxapi/types/chats.py +++ b/maxapi/types/chats.py @@ -3,6 +3,7 @@ # нужен в рантайме: get_type_hints() не найдёт `builtins`, # если импорт только под TYPE_CHECKING import builtins # noqa: TC003 +import warnings from datetime import datetime from typing import TYPE_CHECKING, Any @@ -86,7 +87,7 @@ async def _walk_member_pages( class ChatMembersManager(BotMixin): - """High-level API для работы с участниками чата.""" + """High-level API для работы с участниками группового чата.""" def __init__(self, *, bot: Bot | None, chat_id: int) -> None: self.bot = bot @@ -113,6 +114,8 @@ async def get(self, user_id: int) -> ChatMember | None: ) async def add(self, user_ids: Sequence[int]) -> AddedMembersChat: + """Добавить участников в групповой чат.""" + return await self._ensure_bot().add_chat_members( chat_id=self.chat_id, user_ids=list(user_ids), @@ -178,10 +181,18 @@ async def add( *, marker: int | None = None, ) -> AddedListAdminChat: + if marker is not None: + warnings.warn( + "Параметр marker в chat.admins.add() устарел и " + "игнорируется: POST /chats/{chatId}/members/admins " + "больше не поддерживает marker.", + DeprecationWarning, + stacklevel=2, + ) + return await self._ensure_bot().add_list_admin_chat( chat_id=self.chat_id, admins=list(admins), - marker=marker, ) async def remove(self, user_id: int) -> RemovedAdmin: @@ -452,7 +463,7 @@ class Chats(BaseModel): class ChatMember(User): """ - Модель участника чата. + Модель участника группового чата или канала. Attributes: last_access_time: Время последнего доступа. diff --git a/tests/test_highlevel_shortcuts.py b/tests/test_highlevel_shortcuts.py index c2fbbe30..6c441c2e 100644 --- a/tests/test_highlevel_shortcuts.py +++ b/tests/test_highlevel_shortcuts.py @@ -192,7 +192,7 @@ async def test_chat_high_level_shortcuts_delegate_to_bot(): bot.get_list_admin_chat.assert_awaited_once_with(100, marker=None) admin_call = bot.add_list_admin_chat.await_args.kwargs assert admin_call["chat_id"] == 100 - assert admin_call["marker"] is None + assert "marker" not in admin_call assert len(admin_call["admins"]) == 1 assert admin_call["admins"][0].user_id == 9 bot.remove_admin.assert_awaited_once_with(chat_id=100, user_id=9) diff --git a/tests/test_swagger_alignment.py b/tests/test_swagger_alignment.py index 914680b9..09845fb3 100644 --- a/tests/test_swagger_alignment.py +++ b/tests/test_swagger_alignment.py @@ -7,6 +7,7 @@ from maxapi.enums.attachment import AttachmentType from maxapi.enums.chat_permission import ChatPermission from maxapi.enums.text_style import TextStyle +from maxapi.methods.add_admin_chat import AddAdminChat from maxapi.methods.get_chat_by_link import GetChatByLink from maxapi.methods.get_messages import GetMessages from maxapi.methods.types.added_members_chat import AddedMembersChat @@ -187,6 +188,68 @@ async def test_get_messages_omits_none_count(bot): assert "count" not in params +async def test_add_admin_chat_sends_only_admins_payload(bot): + admin = ChatAdmin( + user_id=1, + permissions=[ChatPermission.READ_ALL_MESSAGES], + ) + method = AddAdminChat(bot=bot, chat_id=123, admins=[admin]) + + with patch.object( + BaseConnection, "request", new=AsyncMock(return_value=Mock()) + ) as mocked_request: + await method.fetch() + + payload = mocked_request.call_args.kwargs["json"] + assert set(payload) == {"admins"} + assert payload["admins"] == [admin.model_dump()] + + +async def test_add_admin_chat_ignores_deprecated_marker(bot): + admin = ChatAdmin( + user_id=1, + permissions=[ChatPermission.READ_ALL_MESSAGES], + ) + + with pytest.warns(DeprecationWarning, match="marker"): + method = AddAdminChat( + bot=bot, + chat_id=123, + admins=[admin], + marker=42, + ) + + with patch.object( + BaseConnection, "request", new=AsyncMock(return_value=Mock()) + ) as mocked_request: + await method.fetch() + + payload = mocked_request.call_args.kwargs["json"] + assert "marker" not in payload + + +async def test_bot_add_list_admin_chat_ignores_deprecated_marker(bot): + admin = ChatAdmin( + user_id=1, + permissions=[ChatPermission.READ_ALL_MESSAGES], + ) + + with ( + patch.object( + BaseConnection, "request", new=AsyncMock(return_value=Mock()) + ) as mocked_request, + pytest.warns(DeprecationWarning, match="marker"), + ): + await bot.add_list_admin_chat( + chat_id=123, + admins=[admin], + marker=42, + ) + + payload = mocked_request.call_args.kwargs["json"] + assert "marker" not in payload + + @pytest.mark.parametrize( ("link", "expected_path"), [ @@ -200,7 +263,8 @@ async def test_get_chat_by_link_normalizes_public_link( link, expected_path, ): - method = GetChatByLink(bot=bot, link=link) + with pytest.warns(DeprecationWarning, match="GetChatByLink"): + method = GetChatByLink(bot=bot, link=link) with patch.object( BaseConnection, "request", new=AsyncMock(return_value=Mock()) @@ -220,12 +284,16 @@ async def test_get_chat_by_link_normalizes_public_link( ], ) def test_get_chat_by_link_rejects_invalid_link(bot, link): - with pytest.raises(ValueError, match="link не соответствует"): + with ( + pytest.warns(DeprecationWarning, match="GetChatByLink"), + pytest.raises(ValueError, match="link не соответствует"), + ): GetChatByLink(bot=bot, link=link) async def test_get_chat_by_link_keeps_valid_link_characters(bot): - method = GetChatByLink(bot=bot, link="channel-name_123") + with pytest.warns(DeprecationWarning, match="GetChatByLink"): + method = GetChatByLink(bot=bot, link="channel-name_123") with patch.object( BaseConnection, "request", new=AsyncMock(return_value=Mock()) From 2af51f8ff0fde4c463e145fc769d2a8079ca3093 Mon Sep 17 00:00:00 2001 From: loveapples Date: Sat, 11 Jul 2026 00:05:51 +0400 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88?= =?UTF-8?q?=D0=B8=D1=85=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=20API=20=D1=87=D0=B0=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_highlevel_shortcuts.py | 23 +++++++++++++++++++++++ tests/test_swagger_alignment.py | 14 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/tests/test_highlevel_shortcuts.py b/tests/test_highlevel_shortcuts.py index 6c441c2e..bdfef1cb 100644 --- a/tests/test_highlevel_shortcuts.py +++ b/tests/test_highlevel_shortcuts.py @@ -198,6 +198,29 @@ async def test_chat_high_level_shortcuts_delegate_to_bot(): bot.remove_admin.assert_awaited_once_with(chat_id=100, user_id=9) +async def test_chat_admins_add_ignores_deprecated_marker(): + """Проверить deprecated marker в high-level API администраторов.""" + bot = ShortcutBot() + chat = Chat( + chat_id=100, + type=ChatType.CHAT, + status=ChatStatus.ACTIVE, + last_event_time=1, + participants_count=1, + is_public=False, + ) + chat.bot = bot + admin = ChatAdmin(user_id=9, permissions=[]) + + with pytest.warns(DeprecationWarning, match="marker"): + await chat.admins.add([admin], marker=42) + + bot.add_list_admin_chat.assert_awaited_once_with( + chat_id=100, + admins=[admin], + ) + + async def test_chat_typing_context_sends_periodic_actions(): bot = ShortcutBot() chat = Chat( diff --git a/tests/test_swagger_alignment.py b/tests/test_swagger_alignment.py index 09845fb3..b3e8b13b 100644 --- a/tests/test_swagger_alignment.py +++ b/tests/test_swagger_alignment.py @@ -305,6 +305,20 @@ async def test_get_chat_by_link_keeps_valid_link_characters(bot): ) +async def test_bot_get_chat_by_link_warns_about_deprecation(bot): + """Проверить предупреждение публичной обёртки устаревшего метода.""" + with ( + patch( + "maxapi.bot.GetChatByLink.fetch", + new=AsyncMock(return_value=Mock()), + ) as mocked_fetch, + pytest.warns(DeprecationWarning, match="get_chat_by_link"), + ): + await bot.get_chat_by_link("channel") + + mocked_fetch.assert_awaited_once_with() + + def test_contact_payload_accepts_hash_and_nullable_vcf(): payload = ContactAttachmentPayload.model_validate( {"vcf_info": None, "hash": "contact-hash", "max_info": None}