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 <latand@users.noreply.github.com>
Co-authored-by: Codex Agent <codex@openclaw.local>
This commit is contained in:
Kostiantyn Kriuchkov 2026-03-02 20:05:25 +02:00 committed by GitHub
parent 251df4b193
commit ebfab22d64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 202 additions and 8 deletions

View file

@ -39,6 +39,7 @@ extract:
- reply_to_story - reply_to_story
- business_connection_id - business_connection_id
- sender_business_bot - sender_business_bot
- sender_tag
- is_from_offline - is_from_offline
- has_media_spoiler - has_media_spoiler
- effect_id - effect_id

View file

@ -71,6 +71,10 @@ set_administrator_custom_title:
method: setChatAdministratorCustomTitle method: setChatAdministratorCustomTitle
fill: *self fill: *self
set_member_tag:
method: setChatMemberTag
fill: *self
set_permissions: set_permissions:
method: setChatPermissions method: setChatPermissions
fill: *self fill: *self

1
CHANGES/1780.feature.rst Normal file
View file

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

View file

@ -8,7 +8,6 @@ class ContentType(str, Enum):
UNKNOWN = "unknown" UNKNOWN = "unknown"
ANY = "any" ANY = "any"
SENDER_TAG = "sender_tag"
TEXT = "text" TEXT = "text"
ANIMATION = "animation" ANIMATION = "animation"
AUDIO = "audio" AUDIO = "audio"

View file

@ -28,6 +28,7 @@ if TYPE_CHECKING:
SendChatAction, SendChatAction,
SetChatAdministratorCustomTitle, SetChatAdministratorCustomTitle,
SetChatDescription, SetChatDescription,
SetChatMemberTag,
SetChatPermissions, SetChatPermissions,
SetChatPhoto, SetChatPhoto,
SetChatStickerSet, SetChatStickerSet,
@ -967,6 +968,38 @@ class Chat(TelegramObject):
**kwargs, **kwargs,
).as_(self._bot) ).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( def set_permissions(
self, self,
permissions: ChatPermissions, permissions: ChatPermissions,

View file

@ -6,6 +6,11 @@ class TestPromoteChatMember:
async def test_bot_method(self, bot: MockedBot): async def test_bot_method(self, bot: MockedBot):
prepare_result = bot.add_result_for(PromoteChatMember, ok=True, result=True) prepare_result = bot.add_result_for(PromoteChatMember, ok=True, result=True)
response: bool = await bot.promote_chat_member(chat_id=-42, user_id=42) response: bool = await bot.promote_chat_member(
bot.get_request() 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 assert response == prepare_result.result

View file

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

View file

@ -115,6 +115,14 @@ class TestChat:
method = chat.set_administrator_custom_title(user_id=1, custom_title="test") method = chat.set_administrator_custom_title(user_id=1, custom_title="test")
assert method.chat_id == chat.id 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): def test_set_permissions(self):
chat = Chat(id=-42, type="supergroup") chat = Chat(id=-42, type="supergroup")

View file

@ -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"

View file

@ -314,6 +314,7 @@ class TestChatMemberUpdatedStatusFilter:
"can_send_polls": True, "can_send_polls": True,
"can_send_other_messages": True, "can_send_other_messages": True,
"can_add_web_page_previews": True, "can_add_web_page_previews": True,
"can_edit_tag": True,
"can_post_stories": True, "can_post_stories": True,
"can_edit_stories": True, "can_edit_stories": True,
"can_delete_stories": True, "can_delete_stories": True,

View file

@ -70,6 +70,7 @@ CHAT_MEMBER_RESTRICTED = ChatMemberRestricted(
can_send_polls=False, can_send_polls=False,
can_send_other_messages=False, can_send_other_messages=False,
can_add_web_page_previews=False, can_add_web_page_previews=False,
can_edit_tag=False,
can_change_info=False, can_change_info=False,
can_invite_users=False, can_invite_users=False,
can_pin_messages=False, can_pin_messages=False,

View file

@ -9,6 +9,7 @@ from aiogram.utils.formatting import (
CashTag, CashTag,
Code, Code,
CustomEmoji, CustomEmoji,
DateTime,
Email, Email,
ExpandableBlockQuote, ExpandableBlockQuote,
HashTag, HashTag,
@ -93,7 +94,7 @@ class TestNode:
], ],
[ [
Pre("test", language="python"), Pre("test", language="python"),
'<pre><code class="language-python">test</code></pre>', '<pre><code language="language-python">test</code></pre>',
], ],
[ [
TextLink("test", url="https://example.com"), TextLink("test", url="https://example.com"),
@ -105,7 +106,7 @@ class TestNode:
], ],
[ [
CustomEmoji("test", custom_emoji_id="42"), CustomEmoji("test", custom_emoji_id="42"),
'<tg-emoji emoji-id="42">test</tg-emoji>', '<tg-emoji emoji_id="42">test</tg-emoji>',
], ],
[ [
BlockQuote("test"), BlockQuote("test"),
@ -115,6 +116,10 @@ class TestNode:
ExpandableBlockQuote("test"), ExpandableBlockQuote("test"),
"<blockquote expandable>test</blockquote>", "<blockquote expandable>test</blockquote>",
], ],
[
DateTime("test", unix_time=42, date_time_format="yMd"),
'<tg-time unix="42" format="yMd">test</tg-time>',
],
], ],
) )
def test_render_plain_only(self, node: Text, result: str): def test_render_plain_only(self, node: Text, result: str):
@ -358,6 +363,22 @@ class TestUtils:
assert isinstance(node, Bold) assert isinstance(node, Bold)
assert node._body == ("test",) 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): def test_as_line(self):
node = as_line("test", "test", "test") node = as_line("test", "test", "test")
assert isinstance(node, Text) assert isinstance(node, Text)

View file

@ -25,7 +25,7 @@ class TestTextDecoration:
[ [
html_decoration, html_decoration,
MessageEntity(type="pre", offset=0, length=5, language="python"), MessageEntity(type="pre", offset=0, length=5, language="python"),
'<pre><code class="language-python">test</code></pre>', '<pre><code language="language-python">test</code></pre>',
], ],
[html_decoration, MessageEntity(type="underline", offset=0, length=5), "<u>test</u>"], [html_decoration, MessageEntity(type="underline", offset=0, length=5), "<u>test</u>"],
[ [
@ -57,7 +57,7 @@ class TestTextDecoration:
[ [
html_decoration, html_decoration,
MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"), MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"),
'<tg-emoji emoji-id="42">test</tg-emoji>', '<tg-emoji emoji_id="42">test</tg-emoji>',
], ],
[ [
html_decoration, html_decoration,
@ -74,6 +74,17 @@ class TestTextDecoration:
MessageEntity(type="expandable_blockquote", offset=0, length=5), MessageEntity(type="expandable_blockquote", offset=0, length=5),
"<blockquote expandable>test</blockquote>", "<blockquote expandable>test</blockquote>",
], ],
[
html_decoration,
MessageEntity(
type="date_time",
offset=0,
length=5,
unix_time=42,
date_time_format="yMd",
),
'<tg-time unix="42" format="yMd">test</tg-time>',
],
[markdown_decoration, MessageEntity(type="bold", offset=0, length=5), "*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="italic", offset=0, length=5), "_\rtest_\r"],
[markdown_decoration, MessageEntity(type="code", offset=0, length=5), "`test`"], [markdown_decoration, MessageEntity(type="code", offset=0, length=5), "`test`"],
@ -102,7 +113,7 @@ class TestTextDecoration:
[ [
markdown_decoration, markdown_decoration,
MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"), 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, markdown_decoration,
@ -124,6 +135,17 @@ class TestTextDecoration:
MessageEntity(type="expandable_blockquote", offset=0, length=5), MessageEntity(type="expandable_blockquote", offset=0, length=5),
">test||", ">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( def test_apply_single_entity(