From ebfab22d643fabd7035d10368a4862cd78bfb63b Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:05:25 +0200 Subject: [PATCH] Draft: follow-up for Bot API 9.5 (#1780) (#1781) * Add set_chat_member_tag shortcut coverage * Add set_member_tag shortcut tests and align decoration expectations * Fix follow-up test coverage for sender_tag and can_edit_tag * Add changelog fragment for PR 1781 * Align changelog with base PR #1780 * Expand 1780 changelog to cover base and follow-up scope * Treat sender_tag as metadata, not message content type --------- Co-authored-by: Latand Co-authored-by: Codex Agent --- .butcher/enums/ContentType.yml | 1 + .butcher/types/Chat/aliases.yml | 4 + CHANGES/1780.feature.rst | 1 + aiogram/enums/content_type.py | 1 - aiogram/types/chat.py | 33 ++++++++ .../test_methods/test_promote_chat_member.py | 9 +- .../test_methods/test_set_chat_member_tag.py | 14 ++++ tests/test_api/test_types/test_chat.py | 8 ++ .../test_chat_member_tag_permissions.py | 84 +++++++++++++++++++ .../test_filters/test_chat_member_updated.py | 1 + tests/test_utils/test_chat_member.py | 1 + tests/test_utils/test_formatting.py | 25 +++++- tests/test_utils/test_text_decorations.py | 28 ++++++- 13 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 CHANGES/1780.feature.rst create mode 100644 tests/test_api/test_methods/test_set_chat_member_tag.py create mode 100644 tests/test_api/test_types/test_chat_member_tag_permissions.py diff --git a/.butcher/enums/ContentType.yml b/.butcher/enums/ContentType.yml index 8d70ad16..acdae0a1 100644 --- a/.butcher/enums/ContentType.yml +++ b/.butcher/enums/ContentType.yml @@ -39,6 +39,7 @@ extract: - reply_to_story - business_connection_id - sender_business_bot + - sender_tag - is_from_offline - has_media_spoiler - effect_id diff --git a/.butcher/types/Chat/aliases.yml b/.butcher/types/Chat/aliases.yml index 89b5843c..7a03c4a9 100644 --- a/.butcher/types/Chat/aliases.yml +++ b/.butcher/types/Chat/aliases.yml @@ -71,6 +71,10 @@ set_administrator_custom_title: method: setChatAdministratorCustomTitle fill: *self +set_member_tag: + method: setChatMemberTag + fill: *self + set_permissions: method: setChatPermissions fill: *self diff --git a/CHANGES/1780.feature.rst b/CHANGES/1780.feature.rst new file mode 100644 index 00000000..d2e5b40e --- /dev/null +++ b/CHANGES/1780.feature.rst @@ -0,0 +1 @@ +Added full Telegram Bot API 9.5 support across aiogram: new API methods (``sendChecklist``, ``sendMessageDraft``, ``setChatMemberTag``, ``editMessageChecklist``), updated chat member/tag capability fields (including ``can_manage_tags``, ``can_edit_tag``, ``tag`` and ``sender_tag``), updated message/content-type and ``MessageEntity`` handling (including date-time entity formatting), regenerated API/docs artifacts, and follow-up shortcut/codegen/test coverage for ``Chat.set_member_tag(...)`` and related tag behaviors. \ No newline at end of file diff --git a/aiogram/enums/content_type.py b/aiogram/enums/content_type.py index b90f84e3..b2a555d4 100644 --- a/aiogram/enums/content_type.py +++ b/aiogram/enums/content_type.py @@ -8,7 +8,6 @@ class ContentType(str, Enum): UNKNOWN = "unknown" ANY = "any" - SENDER_TAG = "sender_tag" TEXT = "text" ANIMATION = "animation" AUDIO = "audio" diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index b956f4ba..cde24548 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: SendChatAction, SetChatAdministratorCustomTitle, SetChatDescription, + SetChatMemberTag, SetChatPermissions, SetChatPhoto, SetChatStickerSet, @@ -967,6 +968,38 @@ class Chat(TelegramObject): **kwargs, ).as_(self._bot) + def set_member_tag( + self, + user_id: int, + tag: str | None = None, + **kwargs: Any, + ) -> SetChatMemberTag: + """ + Shortcut for method :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` + will automatically fill method attributes: + + - :code:`chat_id` + + Use this method to set a tag for a regular member in a group or a supergroup. The bot must be an administrator in the chat for this to work and must have the *can_manage_tags* administrator right. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setchatmembertag + + :param user_id: Unique identifier of the target user + :param tag: New tag for the member; 0-16 characters, emoji are not allowed + :return: instance of method :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` + """ + # DO NOT EDIT MANUALLY!!! + # This method was auto-generated via `butcher` + + from aiogram.methods import SetChatMemberTag + + return SetChatMemberTag( + chat_id=self.id, + user_id=user_id, + tag=tag, + **kwargs, + ).as_(self._bot) + def set_permissions( self, permissions: ChatPermissions, diff --git a/tests/test_api/test_methods/test_promote_chat_member.py b/tests/test_api/test_methods/test_promote_chat_member.py index ee3b7f4e..1f19f3da 100644 --- a/tests/test_api/test_methods/test_promote_chat_member.py +++ b/tests/test_api/test_methods/test_promote_chat_member.py @@ -6,6 +6,11 @@ class TestPromoteChatMember: async def test_bot_method(self, bot: MockedBot): prepare_result = bot.add_result_for(PromoteChatMember, ok=True, result=True) - response: bool = await bot.promote_chat_member(chat_id=-42, user_id=42) - bot.get_request() + response: bool = await bot.promote_chat_member( + chat_id=-42, + user_id=42, + can_manage_tags=True, + ) + request = bot.get_request() + assert request.can_manage_tags is True assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_set_chat_member_tag.py b/tests/test_api/test_methods/test_set_chat_member_tag.py new file mode 100644 index 00000000..edc581cd --- /dev/null +++ b/tests/test_api/test_methods/test_set_chat_member_tag.py @@ -0,0 +1,14 @@ +from aiogram.methods import SetChatMemberTag +from tests.mocked_bot import MockedBot + + +class TestSetChatMemberTag: + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetChatMemberTag, ok=True, result=True) + + response: bool = await bot.set_chat_member_tag(chat_id=-42, user_id=42, tag="test") + request = bot.get_request() + assert request.chat_id == -42 + assert request.user_id == 42 + assert request.tag == "test" + assert response == prepare_result.result diff --git a/tests/test_api/test_types/test_chat.py b/tests/test_api/test_types/test_chat.py index 360b2ee1..b3e63854 100644 --- a/tests/test_api/test_types/test_chat.py +++ b/tests/test_api/test_types/test_chat.py @@ -115,6 +115,14 @@ class TestChat: method = chat.set_administrator_custom_title(user_id=1, custom_title="test") assert method.chat_id == chat.id + def test_set_member_tag(self): + chat = Chat(id=-42, type="supergroup") + + method = chat.set_member_tag(user_id=42, tag="test") + assert method.chat_id == chat.id + assert method.user_id == 42 + assert method.tag == "test" + def test_set_permissions(self): chat = Chat(id=-42, type="supergroup") diff --git a/tests/test_api/test_types/test_chat_member_tag_permissions.py b/tests/test_api/test_types/test_chat_member_tag_permissions.py new file mode 100644 index 00000000..30aa5481 --- /dev/null +++ b/tests/test_api/test_types/test_chat_member_tag_permissions.py @@ -0,0 +1,84 @@ +from datetime import datetime + +from aiogram.types import ( + ChatAdministratorRights, + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberRestricted, + ChatPermissions, + User, +) + + +class TestChatMemberTagPermissions: + def test_chat_administrator_rights_can_manage_tags(self): + rights = ChatAdministratorRights( + is_anonymous=False, + can_manage_chat=True, + can_delete_messages=True, + can_manage_video_chats=True, + can_restrict_members=True, + can_promote_members=True, + can_change_info=True, + can_invite_users=True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + can_manage_tags=True, + ) + assert rights.can_manage_tags is True + + def test_chat_member_administrator_can_manage_tags(self): + admin = ChatMemberAdministrator( + user=User(id=42, is_bot=False, first_name="User"), + can_be_edited=True, + is_anonymous=False, + can_manage_chat=True, + can_delete_messages=True, + can_manage_video_chats=True, + can_restrict_members=True, + can_promote_members=True, + can_change_info=True, + can_invite_users=True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + can_manage_tags=True, + ) + assert admin.can_manage_tags is True + + def test_chat_permissions_can_edit_tag(self): + permissions = ChatPermissions(can_edit_tag=True) + assert permissions.can_edit_tag is True + + def test_chat_member_member_tag(self): + member = ChatMemberMember( + user=User(id=42, is_bot=False, first_name="User"), + tag="premium", + ) + assert member.tag == "premium" + + def test_chat_member_restricted_can_edit_tag_and_tag(self): + restricted = ChatMemberRestricted( + user=User(id=42, is_bot=False, first_name="User"), + is_member=True, + can_send_messages=True, + can_send_audios=True, + can_send_documents=True, + can_send_photos=True, + can_send_videos=True, + can_send_video_notes=True, + can_send_voice_notes=True, + can_send_polls=True, + can_send_other_messages=True, + can_add_web_page_previews=True, + can_edit_tag=True, + can_change_info=True, + can_invite_users=True, + can_pin_messages=True, + can_manage_topics=True, + until_date=datetime.now(), + tag="premium", + ) + assert restricted.can_edit_tag is True + assert restricted.tag == "premium" diff --git a/tests/test_filters/test_chat_member_updated.py b/tests/test_filters/test_chat_member_updated.py index c88b705e..4582f052 100644 --- a/tests/test_filters/test_chat_member_updated.py +++ b/tests/test_filters/test_chat_member_updated.py @@ -314,6 +314,7 @@ class TestChatMemberUpdatedStatusFilter: "can_send_polls": True, "can_send_other_messages": True, "can_add_web_page_previews": True, + "can_edit_tag": True, "can_post_stories": True, "can_edit_stories": True, "can_delete_stories": True, diff --git a/tests/test_utils/test_chat_member.py b/tests/test_utils/test_chat_member.py index 8a42600c..34f32d1c 100644 --- a/tests/test_utils/test_chat_member.py +++ b/tests/test_utils/test_chat_member.py @@ -70,6 +70,7 @@ CHAT_MEMBER_RESTRICTED = ChatMemberRestricted( can_send_polls=False, can_send_other_messages=False, can_add_web_page_previews=False, + can_edit_tag=False, can_change_info=False, can_invite_users=False, can_pin_messages=False, diff --git a/tests/test_utils/test_formatting.py b/tests/test_utils/test_formatting.py index ffaef31e..5928fc62 100644 --- a/tests/test_utils/test_formatting.py +++ b/tests/test_utils/test_formatting.py @@ -9,6 +9,7 @@ from aiogram.utils.formatting import ( CashTag, Code, CustomEmoji, + DateTime, Email, ExpandableBlockQuote, HashTag, @@ -93,7 +94,7 @@ class TestNode: ], [ Pre("test", language="python"), - '
test
', + '
test
', ], [ TextLink("test", url="https://example.com"), @@ -105,7 +106,7 @@ class TestNode: ], [ CustomEmoji("test", custom_emoji_id="42"), - 'test', + 'test', ], [ BlockQuote("test"), @@ -115,6 +116,10 @@ class TestNode: ExpandableBlockQuote("test"), "
test
", ], + [ + DateTime("test", unix_time=42, date_time_format="yMd"), + 'test', + ], ], ) def test_render_plain_only(self, node: Text, result: str): @@ -358,6 +363,22 @@ class TestUtils: assert isinstance(node, Bold) assert node._body == ("test",) + def test_apply_entity_date_time(self): + node = _apply_entity( + MessageEntity( + type=MessageEntityType.DATE_TIME, + offset=0, + length=4, + unix_time=42, + date_time_format="yMd", + ), + "test", + ) + assert isinstance(node, DateTime) + assert node._body == ("test",) + assert node._params["unix_time"] == 42 + assert node._params["date_time_format"] == "yMd" + def test_as_line(self): node = as_line("test", "test", "test") assert isinstance(node, Text) diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py index b4ccb5e8..82039229 100644 --- a/tests/test_utils/test_text_decorations.py +++ b/tests/test_utils/test_text_decorations.py @@ -25,7 +25,7 @@ class TestTextDecoration: [ html_decoration, MessageEntity(type="pre", offset=0, length=5, language="python"), - '
test
', + '
test
', ], [html_decoration, MessageEntity(type="underline", offset=0, length=5), "test"], [ @@ -57,7 +57,7 @@ class TestTextDecoration: [ html_decoration, MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"), - 'test', + 'test', ], [ html_decoration, @@ -74,6 +74,17 @@ class TestTextDecoration: MessageEntity(type="expandable_blockquote", offset=0, length=5), "
test
", ], + [ + html_decoration, + MessageEntity( + type="date_time", + offset=0, + length=5, + unix_time=42, + date_time_format="yMd", + ), + 'test', + ], [markdown_decoration, MessageEntity(type="bold", offset=0, length=5), "*test*"], [markdown_decoration, MessageEntity(type="italic", offset=0, length=5), "_\rtest_\r"], [markdown_decoration, MessageEntity(type="code", offset=0, length=5), "`test`"], @@ -102,7 +113,7 @@ class TestTextDecoration: [ markdown_decoration, MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"), - "![test](tg://emoji?id=42)", + "![test](tg://emoji?emoji_id=42)", ], [ markdown_decoration, @@ -124,6 +135,17 @@ class TestTextDecoration: MessageEntity(type="expandable_blockquote", offset=0, length=5), ">test||", ], + [ + markdown_decoration, + MessageEntity( + type="date_time", + offset=0, + length=5, + unix_time=42, + date_time_format="yMd", + ), + "![test](tg://time?unix=42&format=yMd)", + ], ], ) def test_apply_single_entity(