`_\n",
"name": "reply_markup"
}
],
diff --git a/.butcher/methods/sendMessageDraft/entity.json b/.butcher/methods/sendMessageDraft/entity.json
index 5d64e874..b84bc368 100644
--- a/.butcher/methods/sendMessageDraft/entity.json
+++ b/.butcher/methods/sendMessageDraft/entity.json
@@ -7,9 +7,9 @@
"object": {
"anchor": "sendmessagedraft",
"name": "sendMessageDraft",
- "description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.",
- "html_description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.
",
- "rst_description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success.",
+ "description": "Use this method to stream a partial message to a user while the message is being generated. Returns True on success.",
+ "html_description": "Use this method to stream a partial message to a user while the message is being generated. Returns True on success.
",
+ "rst_description": "Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success.",
"annotations": [
{
"type": "Integer",
diff --git a/.butcher/methods/setChatMemberTag/entity.json b/.butcher/methods/setChatMemberTag/entity.json
new file mode 100644
index 00000000..5de6b59f
--- /dev/null
+++ b/.butcher/methods/setChatMemberTag/entity.json
@@ -0,0 +1,41 @@
+{
+ "meta": {},
+ "group": {
+ "title": "Available methods",
+ "anchor": "available-methods"
+ },
+ "object": {
+ "anchor": "setchatmembertag",
+ "name": "setChatMemberTag",
+ "description": "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 True on success.",
+ "html_description": "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 True on success.
",
+ "rst_description": "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.",
+ "annotations": [
+ {
+ "type": "Integer or String",
+ "required": true,
+ "description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername)",
+ "html_description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) | ",
+ "rst_description": "Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)\n",
+ "name": "chat_id"
+ },
+ {
+ "type": "Integer",
+ "required": true,
+ "description": "Unique identifier of the target user",
+ "html_description": "Unique identifier of the target user | ",
+ "rst_description": "Unique identifier of the target user\n",
+ "name": "user_id"
+ },
+ {
+ "type": "String",
+ "required": false,
+ "description": "New tag for the member; 0-16 characters, emoji are not allowed",
+ "html_description": "New tag for the member; 0-16 characters, emoji are not allowed | ",
+ "rst_description": "New tag for the member; 0-16 characters, emoji are not allowed\n",
+ "name": "tag"
+ }
+ ],
+ "category": "methods"
+ }
+}
diff --git a/.butcher/schema/schema.json b/.butcher/schema/schema.json
index 0fca3099..faa2aee0 100644
--- a/.butcher/schema/schema.json
+++ b/.butcher/schema/schema.json
@@ -1,7 +1,7 @@
{
"api": {
- "version": "9.4",
- "release_date": "2026-02-09"
+ "version": "9.5",
+ "release_date": "2026-03-01"
},
"items": [
{
@@ -1119,6 +1119,14 @@
"name": "sender_business_bot",
"required": false
},
+ {
+ "type": "String",
+ "description": "Tag or custom title of the sender of the message; for supergroups only",
+ "html_description": "Optional. Tag or custom title of the sender of the message; for supergroups only | ",
+ "rst_description": "*Optional*. Tag or custom title of the sender of the message; for supergroups only\n",
+ "name": "sender_tag",
+ "required": false
+ },
{
"type": "Integer",
"description": "Date the message was sent in Unix time. It is always a positive number, representing a valid date.",
@@ -1249,9 +1257,9 @@
},
{
"type": "String",
- "description": "The unique identifier of a media message group this message belongs to",
- "html_description": "Optional. The unique identifier of a media message group this message belongs to | ",
- "rst_description": "*Optional*. The unique identifier of a media message group this message belongs to\n",
+ "description": "The unique identifier inside this chat of a media message group this message belongs to",
+ "html_description": "Optional. The unique identifier inside this chat of a media message group this message belongs to | ",
+ "rst_description": "*Optional*. The unique identifier inside this chat of a media message group this message belongs to\n",
"name": "media_group_id",
"required": false
},
@@ -1898,8 +1906,8 @@
{
"type": "InlineKeyboardMarkup",
"description": "Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.",
- "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. | ",
- "rst_description": "*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n",
+ "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. | ",
+ "rst_description": "*Optional*. `Inline keyboard `_ attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n",
"name": "reply_markup",
"required": false
}
@@ -1976,9 +1984,9 @@
"annotations": [
{
"type": "String",
- "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers)",
- "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers) | ",
- "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers)\n",
+ "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)",
+ "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers), or “date_time” (for formatted date and time) | ",
+ "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)\n",
"name": "type",
"required": true
},
@@ -2029,6 +2037,22 @@
"rst_description": "*Optional*. For 'custom_emoji' only, unique identifier of the custom emoji. Use :class:`aiogram.methods.get_custom_emoji_stickers.GetCustomEmojiStickers` to get full information about the sticker\n",
"name": "custom_emoji_id",
"required": false
+ },
+ {
+ "type": "Integer",
+ "description": "For 'date_time' only, the Unix time associated with the entity",
+ "html_description": "Optional. For “date_time” only, the Unix time associated with the entity | ",
+ "rst_description": "*Optional*. For 'date_time' only, the Unix time associated with the entity\n",
+ "name": "unix_time",
+ "required": false
+ },
+ {
+ "type": "String",
+ "description": "For 'date_time' only, the string that defines the formatting of the date and time. See date-time entity formatting for more details.",
+ "html_description": "Optional. For “date_time” only, the string that defines the formatting of the date and time. See date-time entity formatting for more details. | ",
+ "rst_description": "*Optional*. For 'date_time' only, the string that defines the formatting of the date and time. See `date-time entity formatting `_ for more details.\n",
+ "name": "date_time_format",
+ "required": false
}
],
"category": "types"
@@ -6332,6 +6356,14 @@
"rst_description": "*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only\n",
"name": "can_manage_direct_messages",
"required": false
+ },
+ {
+ "type": "Boolean",
+ "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.",
+ "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages. | ",
+ "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n",
+ "name": "can_manage_tags",
+ "required": false
}
],
"category": "types"
@@ -6620,6 +6652,14 @@
"name": "can_manage_direct_messages",
"required": false
},
+ {
+ "type": "Boolean",
+ "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.",
+ "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages. | ",
+ "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n",
+ "name": "can_manage_tags",
+ "required": false
+ },
{
"type": "String",
"description": "Custom title for this user",
@@ -6646,6 +6686,14 @@
"name": "status",
"required": true
},
+ {
+ "type": "String",
+ "description": "Tag of the member",
+ "html_description": "Optional. Tag of the member | ",
+ "rst_description": "*Optional*. Tag of the member\n",
+ "name": "tag",
+ "required": false
+ },
{
"type": "User",
"description": "Information about the user",
@@ -6680,6 +6728,14 @@
"name": "status",
"required": true
},
+ {
+ "type": "String",
+ "description": "Tag of the member",
+ "html_description": "Optional. Tag of the member | ",
+ "rst_description": "*Optional*. Tag of the member\n",
+ "name": "tag",
+ "required": false
+ },
{
"type": "User",
"description": "Information about the user",
@@ -6776,6 +6832,14 @@
"name": "can_add_web_page_previews",
"required": true
},
+ {
+ "type": "Boolean",
+ "description": "True, if the user is allowed to edit their own tag",
+ "html_description": "True, if the user is allowed to edit their own tag | ",
+ "rst_description": ":code:`True`, if the user is allowed to edit their own tag\n",
+ "name": "can_edit_tag",
+ "required": true
+ },
{
"type": "Boolean",
"description": "True, if the user is allowed to change the chat title, photo and other settings",
@@ -7024,6 +7088,14 @@
"name": "can_add_web_page_previews",
"required": false
},
+ {
+ "type": "Boolean",
+ "description": "True, if the user is allowed to edit their own tag",
+ "html_description": "Optional. True, if the user is allowed to edit their own tag | ",
+ "rst_description": "*Optional*. :code:`True`, if the user is allowed to edit their own tag\n",
+ "name": "can_edit_tag",
+ "required": false
+ },
{
"type": "Boolean",
"description": "True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups",
@@ -12959,8 +13031,8 @@
"type": "InlineKeyboardMarkup",
"required": false,
"description": "A JSON-serialized object for an inline keyboard",
- "html_description": "A JSON-serialized object for an inline keyboard | ",
- "rst_description": "A JSON-serialized object for an inline keyboard\n",
+ "html_description": "A JSON-serialized object for an inline keyboard | ",
+ "rst_description": "A JSON-serialized object for an `inline keyboard `_\n",
"name": "reply_markup"
}
],
@@ -13075,9 +13147,9 @@
{
"anchor": "sendmessagedraft",
"name": "sendMessageDraft",
- "description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.",
- "html_description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns True on success.
",
- "rst_description": "Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success.",
+ "description": "Use this method to stream a partial message to a user while the message is being generated. Returns True on success.",
+ "html_description": "Use this method to stream a partial message to a user while the message is being generated. Returns True on success.
",
+ "rst_description": "Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success.",
"annotations": [
{
"type": "Integer",
@@ -13610,6 +13682,14 @@
"html_description": "Pass True if the administrator can manage direct messages within the channel and decline suggested posts; for channels only | ",
"rst_description": "Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only\n",
"name": "can_manage_direct_messages"
+ },
+ {
+ "type": "Boolean",
+ "required": false,
+ "description": "Pass True if the administrator can edit the tags of regular members; for groups and supergroups only",
+ "html_description": "Pass True if the administrator can edit the tags of regular members; for groups and supergroups only | ",
+ "rst_description": "Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only\n",
+ "name": "can_manage_tags"
}
],
"category": "methods"
@@ -13648,6 +13728,40 @@
],
"category": "methods"
},
+ {
+ "anchor": "setchatmembertag",
+ "name": "setChatMemberTag",
+ "description": "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 True on success.",
+ "html_description": "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 True on success.
",
+ "rst_description": "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.",
+ "annotations": [
+ {
+ "type": "Integer or String",
+ "required": true,
+ "description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername)",
+ "html_description": "Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) | ",
+ "rst_description": "Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)\n",
+ "name": "chat_id"
+ },
+ {
+ "type": "Integer",
+ "required": true,
+ "description": "Unique identifier of the target user",
+ "html_description": "Unique identifier of the target user | ",
+ "rst_description": "Unique identifier of the target user\n",
+ "name": "user_id"
+ },
+ {
+ "type": "String",
+ "required": false,
+ "description": "New tag for the member; 0-16 characters, emoji are not allowed",
+ "html_description": "New tag for the member; 0-16 characters, emoji are not allowed | ",
+ "rst_description": "New tag for the member; 0-16 characters, emoji are not allowed\n",
+ "name": "tag"
+ }
+ ],
+ "category": "methods"
+ },
{
"anchor": "banchatsenderchat",
"name": "banChatSenderChat",
@@ -16631,8 +16745,8 @@
"type": "InlineKeyboardMarkup",
"required": false,
"description": "A JSON-serialized object for the new inline keyboard for the message",
- "html_description": "A JSON-serialized object for the new inline keyboard for the message | ",
- "rst_description": "A JSON-serialized object for the new inline keyboard for the message\n",
+ "html_description": "A JSON-serialized object for the new inline keyboard for the message | ",
+ "rst_description": "A JSON-serialized object for the new `inline keyboard `_ for the message\n",
"name": "reply_markup"
}
],
@@ -18727,8 +18841,8 @@
{
"type": "InlineKeyboardMarkup",
"description": "Inline keyboard attached to the message",
- "html_description": "Optional. Inline keyboard attached to the message | ",
- "rst_description": "*Optional*. Inline keyboard attached to the message\n",
+ "html_description": "Optional. Inline keyboard attached to the message | ",
+ "rst_description": "*Optional*. `Inline keyboard `_ attached to the message\n",
"name": "reply_markup",
"required": false
},
@@ -22865,9 +22979,9 @@
{
"anchor": "gamehighscore",
"name": "GameHighScore",
- "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ\n-",
- "html_description": "This object represents one row of the high scores table for a game.
And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »
\n-
",
- "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**\n\n-",
+ "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ",
+ "html_description": "This object represents one row of the high scores table for a game.
And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »
",
+ "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**",
"annotations": [
{
"type": "Integer",
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/.butcher/types/ChatAdministratorRights/entity.json b/.butcher/types/ChatAdministratorRights/entity.json
index 45ebc3b5..f271d1fd 100644
--- a/.butcher/types/ChatAdministratorRights/entity.json
+++ b/.butcher/types/ChatAdministratorRights/entity.json
@@ -138,6 +138,14 @@
"rst_description": "*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only\n",
"name": "can_manage_direct_messages",
"required": false
+ },
+ {
+ "type": "Boolean",
+ "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.",
+ "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages. | ",
+ "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n",
+ "name": "can_manage_tags",
+ "required": false
}
],
"category": "types"
diff --git a/.butcher/types/ChatMemberAdministrator/entity.json b/.butcher/types/ChatMemberAdministrator/entity.json
index f1278554..7b55cc7a 100644
--- a/.butcher/types/ChatMemberAdministrator/entity.json
+++ b/.butcher/types/ChatMemberAdministrator/entity.json
@@ -163,6 +163,14 @@
"name": "can_manage_direct_messages",
"required": false
},
+ {
+ "type": "Boolean",
+ "description": "True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.",
+ "html_description": "Optional. True, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages. | ",
+ "rst_description": "*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages.\n",
+ "name": "can_manage_tags",
+ "required": false
+ },
{
"type": "String",
"description": "Custom title for this user",
diff --git a/.butcher/types/ChatMemberMember/entity.json b/.butcher/types/ChatMemberMember/entity.json
index c9988ee7..ed1a8304 100644
--- a/.butcher/types/ChatMemberMember/entity.json
+++ b/.butcher/types/ChatMemberMember/entity.json
@@ -19,6 +19,14 @@
"name": "status",
"required": true
},
+ {
+ "type": "String",
+ "description": "Tag of the member",
+ "html_description": "Optional. Tag of the member | ",
+ "rst_description": "*Optional*. Tag of the member\n",
+ "name": "tag",
+ "required": false
+ },
{
"type": "User",
"description": "Information about the user",
diff --git a/.butcher/types/ChatMemberRestricted/entity.json b/.butcher/types/ChatMemberRestricted/entity.json
index 75ea1fc0..f0572284 100644
--- a/.butcher/types/ChatMemberRestricted/entity.json
+++ b/.butcher/types/ChatMemberRestricted/entity.json
@@ -19,6 +19,14 @@
"name": "status",
"required": true
},
+ {
+ "type": "String",
+ "description": "Tag of the member",
+ "html_description": "Optional. Tag of the member | ",
+ "rst_description": "*Optional*. Tag of the member\n",
+ "name": "tag",
+ "required": false
+ },
{
"type": "User",
"description": "Information about the user",
@@ -115,6 +123,14 @@
"name": "can_add_web_page_previews",
"required": true
},
+ {
+ "type": "Boolean",
+ "description": "True, if the user is allowed to edit their own tag",
+ "html_description": "True, if the user is allowed to edit their own tag | ",
+ "rst_description": ":code:`True`, if the user is allowed to edit their own tag\n",
+ "name": "can_edit_tag",
+ "required": true
+ },
{
"type": "Boolean",
"description": "True, if the user is allowed to change the chat title, photo and other settings",
diff --git a/.butcher/types/ChatPermissions/entity.json b/.butcher/types/ChatPermissions/entity.json
index c488ef9f..d6ad3cc9 100644
--- a/.butcher/types/ChatPermissions/entity.json
+++ b/.butcher/types/ChatPermissions/entity.json
@@ -91,6 +91,14 @@
"name": "can_add_web_page_previews",
"required": false
},
+ {
+ "type": "Boolean",
+ "description": "True, if the user is allowed to edit their own tag",
+ "html_description": "Optional. True, if the user is allowed to edit their own tag | ",
+ "rst_description": "*Optional*. :code:`True`, if the user is allowed to edit their own tag\n",
+ "name": "can_edit_tag",
+ "required": false
+ },
{
"type": "Boolean",
"description": "True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups",
diff --git a/.butcher/types/GameHighScore/entity.json b/.butcher/types/GameHighScore/entity.json
index 21a8a5e7..ce3f52d2 100644
--- a/.butcher/types/GameHighScore/entity.json
+++ b/.butcher/types/GameHighScore/entity.json
@@ -7,9 +7,9 @@
"object": {
"anchor": "gamehighscore",
"name": "GameHighScore",
- "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ\n-",
- "html_description": "This object represents one row of the high scores table for a game.
And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »
\n-
",
- "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**\n\n-",
+ "description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\nIf you've got any questions, please check out our Bot FAQ",
+ "html_description": "This object represents one row of the high scores table for a game.
And that's about all we've got for now.
\nIf you've got any questions, please check out our Bot FAQ »
",
+ "rst_description": "This object represents one row of the high scores table for a game.\nAnd that's about all we've got for now.\n\nIf you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**",
"annotations": [
{
"type": "Integer",
diff --git a/.butcher/types/InlineQueryResultDocument/entity.json b/.butcher/types/InlineQueryResultDocument/entity.json
index ff703482..97bb05a6 100644
--- a/.butcher/types/InlineQueryResultDocument/entity.json
+++ b/.butcher/types/InlineQueryResultDocument/entity.json
@@ -86,8 +86,8 @@
{
"type": "InlineKeyboardMarkup",
"description": "Inline keyboard attached to the message",
- "html_description": "Optional. Inline keyboard attached to the message | ",
- "rst_description": "*Optional*. Inline keyboard attached to the message\n",
+ "html_description": "Optional. Inline keyboard attached to the message | ",
+ "rst_description": "*Optional*. `Inline keyboard `_ attached to the message\n",
"name": "reply_markup",
"required": false
},
diff --git a/.butcher/types/Message/entity.json b/.butcher/types/Message/entity.json
index 594442fc..7225d275 100644
--- a/.butcher/types/Message/entity.json
+++ b/.butcher/types/Message/entity.json
@@ -67,6 +67,14 @@
"name": "sender_business_bot",
"required": false
},
+ {
+ "type": "String",
+ "description": "Tag or custom title of the sender of the message; for supergroups only",
+ "html_description": "Optional. Tag or custom title of the sender of the message; for supergroups only | ",
+ "rst_description": "*Optional*. Tag or custom title of the sender of the message; for supergroups only\n",
+ "name": "sender_tag",
+ "required": false
+ },
{
"type": "Integer",
"description": "Date the message was sent in Unix time. It is always a positive number, representing a valid date.",
@@ -197,9 +205,9 @@
},
{
"type": "String",
- "description": "The unique identifier of a media message group this message belongs to",
- "html_description": "Optional. The unique identifier of a media message group this message belongs to | ",
- "rst_description": "*Optional*. The unique identifier of a media message group this message belongs to\n",
+ "description": "The unique identifier inside this chat of a media message group this message belongs to",
+ "html_description": "Optional. The unique identifier inside this chat of a media message group this message belongs to | ",
+ "rst_description": "*Optional*. The unique identifier inside this chat of a media message group this message belongs to\n",
"name": "media_group_id",
"required": false
},
@@ -846,8 +854,8 @@
{
"type": "InlineKeyboardMarkup",
"description": "Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons.",
- "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. | ",
- "rst_description": "*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n",
+ "html_description": "Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. | ",
+ "rst_description": "*Optional*. `Inline keyboard `_ attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.\n",
"name": "reply_markup",
"required": false
},
diff --git a/.butcher/types/MessageEntity/entity.json b/.butcher/types/MessageEntity/entity.json
index 8d5a1d13..37dc6cf4 100644
--- a/.butcher/types/MessageEntity/entity.json
+++ b/.butcher/types/MessageEntity/entity.json
@@ -13,9 +13,9 @@
"annotations": [
{
"type": "String",
- "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers)",
- "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers) | ",
- "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers)\n",
+ "description": "Type of the entity. Currently, can be 'mention' (@username), 'hashtag' (#hashtag or #hashtag@chatusername), 'cashtag' ($USD or $USD@chatusername), 'bot_command' (/start@jobs_bot), 'url' (https://telegram.org), 'email' (do-not-reply@telegram.org), 'phone_number' (+1-212-555-0123), 'bold' (bold text), 'italic' (italic text), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users without usernames), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)",
+ "html_description": "Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag or #hashtag@chatusername), “cashtag” ($USD or $USD@chatusername), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “blockquote” (block quotation), “expandable_blockquote” (collapsed-by-default block quotation), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers), or “date_time” (for formatted date and time) | ",
+ "rst_description": "Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)\n",
"name": "type",
"required": true
},
@@ -66,6 +66,22 @@
"rst_description": "*Optional*. For 'custom_emoji' only, unique identifier of the custom emoji. Use :class:`aiogram.methods.get_custom_emoji_stickers.GetCustomEmojiStickers` to get full information about the sticker\n",
"name": "custom_emoji_id",
"required": false
+ },
+ {
+ "type": "Integer",
+ "description": "For 'date_time' only, the Unix time associated with the entity",
+ "html_description": "Optional. For “date_time” only, the Unix time associated with the entity | ",
+ "rst_description": "*Optional*. For 'date_time' only, the Unix time associated with the entity\n",
+ "name": "unix_time",
+ "required": false
+ },
+ {
+ "type": "String",
+ "description": "For 'date_time' only, the string that defines the formatting of the date and time. See date-time entity formatting for more details.",
+ "html_description": "Optional. For “date_time” only, the string that defines the formatting of the date and time. See date-time entity formatting for more details. | ",
+ "rst_description": "*Optional*. For 'date_time' only, the string that defines the formatting of the date and time. See `date-time entity formatting `_ for more details.\n",
+ "name": "date_time_format",
+ "required": false
}
],
"category": "types"
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..42e9a383
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,120 @@
+# AGENTS.md
+
+This file defines how coding agents should contribute to `aiogram` on `dev-3.x`.
+
+## Scope and defaults
+
+- Base branch: `dev-3.x`
+- Python: `>=3.10`
+- Main tooling: `uv`, `ruff`, `mypy`, `pytest`, `towncrier`, `butcher`
+- Keep diffs focused; avoid unrelated refactors/reformatting.
+
+## Setup
+
+```bash
+uv sync --all-extras --group dev --group test
+uv run pre-commit install
+```
+
+Note: `uv run pre-commit install` writes hooks to the shared repository `.git/hooks`
+(common for all worktrees), not only for the current worktree.
+
+## Mandatory local checks before PR
+
+Code style/lint in this repository is enforced via Ruff (`ruff check` + `ruff format`).
+
+Quick loop (recommended for most PR iterations):
+
+```bash
+uv run ruff check --show-fixes --preview aiogram examples
+uv run ruff format --check --diff aiogram tests scripts examples
+uv run mypy aiogram
+uv run pytest tests
+```
+
+Full loop (run before final review request):
+
+```bash
+# Run quick loop first, then:
+uv run pytest --redis redis://:/ tests # when Redis storage paths are affected
+uv run pytest --mongo mongodb://:@: tests # when Mongo storage paths are affected
+uv run --extra docs bash -c 'cd docs && make html' # when docs or generated API docs are affected
+```
+
+If changes touch Redis/Mongo storage behavior, run integration variants too:
+
+```bash
+uv run pytest --redis redis://:/ tests
+uv run pytest --mongo mongodb://:@: tests
+```
+
+Run these only if you have accessible Redis/Mongo instances in your environment.
+
+
+## Changelog rules (CI-gated)
+
+- Add `CHANGES/..rst` unless PR has `skip news` label.
+- Valid categories: `feature`, `bugfix`, `doc`, `removal`, `misc`.
+- Changelog text must describe user-visible behavior changes, not process/org details.
+- Do not edit `CHANGES.rst` directly for regular PRs.
+
+## Bot API/codegen workflow (critical)
+
+`aiogram` API layers are generated. For Bot API related work:
+
+- Prefer editing generator inputs (`.butcher/**/*.yml`, aliases, templates) instead of hand-editing generated code.
+- Do not manually edit `.butcher/**/entity.json` (parser/codegen will overwrite it).
+- For new shortcuts, add alias/config in `.butcher` and regenerate.
+- Regeneration flow:
+
+```bash
+uv run --extra cli butcher parse
+uv run --extra cli butcher refresh
+uv run --extra cli butcher apply all
+```
+
+For maintainers preparing an API/version bump only:
+
+```bash
+make update-api args=patch
+```
+
+`make update-api args=...` also runs version bump scripts and updates version-related files
+(`aiogram/__meta__.py`, `README.rst`, `docs/index.rst`).
+
+After regeneration, run lint/type/tests again.
+
+## Maintainer review signals (recent PRs)
+
+These patterns repeatedly appeared in maintainer feedback and should be treated as hard constraints:
+
+- Keep generation path consistent: shortcuts/features should be added through `.butcher` config + generation, not ad-hoc manual edits.
+- Keep test style consistent with existing suite; avoid introducing new dependencies for small tests.
+- Preserve framework contracts (e.g., dispatcher/workflow data passed to startup/shutdown callbacks).
+- When fixing generated API metadata/docs, update the source mapping in `.butcher` so future regenerations keep the fix.
+
+## Documentation work
+
+For docs changes:
+
+```bash
+uv run --extra docs sphinx-autobuild --watch aiogram/ --watch CHANGES.rst --watch README.rst docs/ docs/_build/
+```
+
+`sphinx-autobuild` is long-running by design.
+
+Or quick build:
+
+```bash
+uv run --extra docs bash -c 'cd docs && make html'
+```
+
+## PR quality checklist
+
+Before requesting review:
+
+1. Tests added/updated for behavior changes.
+2. Local lint/type/tests pass.
+3. Changelog fragment added (or `skip news` is justified).
+4. If codegen-related: generated files and source config are both updated coherently.
+5. PR body includes clear reproduction/validation steps.
diff --git a/CHANGES.rst b/CHANGES.rst
index be4cb56e..9888246a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -16,6 +16,43 @@ Changelog
.. towncrier release notes start
+3.25.0 (2026-03-03)
+====================
+
+Bugfixes
+--------
+
+- Fixed scene transitions to preserve middleware-injected data when moving between scenes via ``SceneWizard.goto``.
+ `#1687 `_
+- Added ``icon_custom_emoji_id`` and ``style`` parameters to ``InlineKeyboardBuilder.button`` and ``ReplyKeyboardBuilder.button`` signatures.
+ `#1768 `_
+- Fixed Pydantic protected namespace warning for `model_custom_emoji_id` by adding `protected_namespaces=()` to `model_config`.
+ `#1772 `_
+
+
+Misc
+----
+
+- Documented webhook security constraints for proxy deployments, including trust requirements for :code:`X-Forwarded-For` and recommended defense-in-depth checks.
+ `#47 `_
+- Updated to `Bot API 9.5 `_
+
+ **New Methods:**
+
+ - Added :class:`aiogram.methods.send_message_draft.SendMessageDraft` method - allowed for all bots to stream partial messages while they are being generated
+ - Added :class:`aiogram.methods.set_chat_member_tag.SetChatMemberTag` method - allows bots to set a custom tag for a chat member; available via :meth:`aiogram.types.chat.Chat.set_member_tag` shortcut
+
+ **New Fields:**
+
+ - Added :code:`date_time` type to :class:`aiogram.types.message_entity.MessageEntity` with :code:`unix_time` and :code:`date_time_format` fields - allows bots to display a formatted date and time to the user
+ - Added :code:`tag` field to :class:`aiogram.types.chat_member_member.ChatMemberMember` and :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` - the custom tag set for the chat member
+ - Added :code:`can_edit_tag` field to :class:`aiogram.types.chat_member_restricted.ChatMemberRestricted` and :class:`aiogram.types.chat_permissions.ChatPermissions` - indicates whether the user is allowed to edit their own tag
+ - Added :code:`can_manage_tags` field to :class:`aiogram.types.chat_member_administrator.ChatMemberAdministrator` and :class:`aiogram.types.chat_administrator_rights.ChatAdministratorRights` - indicates whether the administrator can manage tags of other chat members
+ - Added :code:`can_manage_tags` parameter to :class:`aiogram.methods.promote_chat_member.PromoteChatMember` method
+ - Added :code:`sender_tag` field to :class:`aiogram.types.message.Message` - the tag of the message sender in the chat
+ `#1780 `_
+
+
3.25.0 (2026-02-10)
====================
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..a97d7a44
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,3 @@
+# CLAUDE.md
+
+Use @AGENTS.md as the source of truth for contribution workflow, checks, and Bot API codegen rules in this repository.
diff --git a/README.rst b/README.rst
index c3b660d3..a04ff797 100644
--- a/README.rst
+++ b/README.rst
@@ -52,7 +52,7 @@ Features
- Asynchronous (`asyncio docs `_, :pep:`492`)
- Has type hints (:pep:`484`) and can be used with `mypy `_
- Supports `PyPy `_
-- Supports `Telegram Bot API 9.4 `_ and gets fast updates to the latest versions of the Bot API
+- Supports `Telegram Bot API 9.5 `_ and gets fast updates to the latest versions of the Bot API
- Telegram Bot API integration code was `autogenerated `_ and can be easily re-generated when API gets updated
- Updates router (Blueprints)
- Has Finite State Machine
diff --git a/aiogram/__meta__.py b/aiogram/__meta__.py
index 0c33f1a9..00647de5 100644
--- a/aiogram/__meta__.py
+++ b/aiogram/__meta__.py
@@ -1,2 +1,2 @@
-__version__ = "3.25.0"
-__api_version__ = "9.4"
+__version__ = "3.26.0"
+__api_version__ = "9.5"
diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py
index d10429f6..195c06dd 100644
--- a/aiogram/client/bot.py
+++ b/aiogram/client/bot.py
@@ -144,6 +144,7 @@ from ..methods import (
SetBusinessAccountUsername,
SetChatAdministratorCustomTitle,
SetChatDescription,
+ SetChatMemberTag,
SetChatMenuButton,
SetChatPermissions,
SetChatPhoto,
@@ -2021,6 +2022,7 @@ class Bot:
can_pin_messages: bool | None = None,
can_manage_topics: bool | None = None,
can_manage_direct_messages: bool | None = None,
+ can_manage_tags: bool | None = None,
request_timeout: int | None = None,
) -> bool:
"""
@@ -2046,6 +2048,7 @@ class Bot:
:param can_pin_messages: Pass :code:`True` if the administrator can pin messages; for supergroups only
:param can_manage_topics: Pass :code:`True` if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only
:param can_manage_direct_messages: Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only
+ :param can_manage_tags: Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only
:param request_timeout: Request timeout
:return: Returns :code:`True` on success.
"""
@@ -2069,6 +2072,7 @@ class Bot:
can_pin_messages=can_pin_messages,
can_manage_topics=can_manage_topics,
can_manage_direct_messages=can_manage_direct_messages,
+ can_manage_tags=can_manage_tags,
)
return await self(call, request_timeout=request_timeout)
@@ -5560,7 +5564,7 @@ class Bot:
:param chat_id: Unique identifier for the target chat
:param message_id: Unique identifier for the target message
:param checklist: A JSON-serialized object for the new checklist
- :param reply_markup: A JSON-serialized object for the new inline keyboard for the message
+ :param reply_markup: A JSON-serialized object for the new `inline keyboard `_ for the message
:param request_timeout: Request timeout
:return: On success, the edited :class:`aiogram.types.message.Message` is returned.
"""
@@ -5614,7 +5618,7 @@ class Bot:
:param protect_content: Protects the contents of the sent message from forwarding and saving
:param message_effect_id: Unique identifier of the message effect to be added to the message
:param reply_parameters: A JSON-serialized object for description of the message to reply to
- :param reply_markup: A JSON-serialized object for an inline keyboard
+ :param reply_markup: A JSON-serialized object for an `inline keyboard `_
:param request_timeout: Request timeout
:return: On success, the sent :class:`aiogram.types.message.Message` is returned.
"""
@@ -5823,7 +5827,7 @@ class Bot:
request_timeout: int | None = None,
) -> bool:
"""
- Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success.
+ Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success.
Source: https://core.telegram.org/bots/api#sendmessagedraft
@@ -5908,3 +5912,29 @@ class Bot:
photo=photo,
)
return await self(call, request_timeout=request_timeout)
+
+ async def set_chat_member_tag(
+ self,
+ chat_id: ChatIdUnion,
+ user_id: int,
+ tag: str | None = None,
+ request_timeout: int | None = None,
+ ) -> bool:
+ """
+ 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 chat_id: Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)
+ :param user_id: Unique identifier of the target user
+ :param tag: New tag for the member; 0-16 characters, emoji are not allowed
+ :param request_timeout: Request timeout
+ :return: Returns :code:`True` on success.
+ """
+
+ call = SetChatMemberTag(
+ chat_id=chat_id,
+ user_id=user_id,
+ tag=tag,
+ )
+ return await self(call, request_timeout=request_timeout)
diff --git a/aiogram/enums/message_entity_type.py b/aiogram/enums/message_entity_type.py
index b67dc039..e1dba489 100644
--- a/aiogram/enums/message_entity_type.py
+++ b/aiogram/enums/message_entity_type.py
@@ -27,3 +27,4 @@ class MessageEntityType(str, Enum):
TEXT_LINK = "text_link"
TEXT_MENTION = "text_mention"
CUSTOM_EMOJI = "custom_emoji"
+ DATE_TIME = "date_time"
diff --git a/aiogram/fsm/scene.py b/aiogram/fsm/scene.py
index da0a52d2..4c7fa72c 100644
--- a/aiogram/fsm/scene.py
+++ b/aiogram/fsm/scene.py
@@ -259,6 +259,7 @@ class SceneHandlerWrapper:
)
raise SceneException(msg) from None
event_update: Update = kwargs["event_update"]
+ scenes.data = {**scenes.data, **kwargs}
scene = self.scene(
wizard=SceneWizard(
scene_config=self.scene.__scene_config__,
@@ -712,6 +713,9 @@ class ScenesManager:
:param kwargs: Additional keyword arguments to pass to the scene's wizard.enter() method.
:return: None
"""
+ if kwargs:
+ self.data = {**self.data, **kwargs}
+
if _check_active:
active_scene = await self._get_active_scene()
if active_scene is not None:
diff --git a/aiogram/methods/__init__.py b/aiogram/methods/__init__.py
index 786e53e5..b4c93d6c 100644
--- a/aiogram/methods/__init__.py
+++ b/aiogram/methods/__init__.py
@@ -126,6 +126,7 @@ from .set_business_account_profile_photo import SetBusinessAccountProfilePhoto
from .set_business_account_username import SetBusinessAccountUsername
from .set_chat_administrator_custom_title import SetChatAdministratorCustomTitle
from .set_chat_description import SetChatDescription
+from .set_chat_member_tag import SetChatMemberTag
from .set_chat_menu_button import SetChatMenuButton
from .set_chat_permissions import SetChatPermissions
from .set_chat_photo import SetChatPhoto
@@ -295,6 +296,7 @@ __all__ = (
"SetBusinessAccountUsername",
"SetChatAdministratorCustomTitle",
"SetChatDescription",
+ "SetChatMemberTag",
"SetChatMenuButton",
"SetChatPermissions",
"SetChatPhoto",
diff --git a/aiogram/methods/edit_message_checklist.py b/aiogram/methods/edit_message_checklist.py
index 6f7bbf7d..83d3815f 100644
--- a/aiogram/methods/edit_message_checklist.py
+++ b/aiogram/methods/edit_message_checklist.py
@@ -25,7 +25,7 @@ class EditMessageChecklist(TelegramMethod[Message]):
checklist: InputChecklist
"""A JSON-serialized object for the new checklist"""
reply_markup: InlineKeyboardMarkup | None = None
- """A JSON-serialized object for the new inline keyboard for the message"""
+ """A JSON-serialized object for the new `inline keyboard `_ for the message"""
if TYPE_CHECKING:
# DO NOT EDIT MANUALLY!!!
diff --git a/aiogram/methods/promote_chat_member.py b/aiogram/methods/promote_chat_member.py
index e26f821e..6f8d4dbf 100644
--- a/aiogram/methods/promote_chat_member.py
+++ b/aiogram/methods/promote_chat_member.py
@@ -52,6 +52,8 @@ class PromoteChatMember(TelegramMethod[bool]):
"""Pass :code:`True` if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only"""
can_manage_direct_messages: bool | None = None
"""Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only"""
+ can_manage_tags: bool | None = None
+ """Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only"""
if TYPE_CHECKING:
# DO NOT EDIT MANUALLY!!!
@@ -78,6 +80,7 @@ class PromoteChatMember(TelegramMethod[bool]):
can_pin_messages: bool | None = None,
can_manage_topics: bool | None = None,
can_manage_direct_messages: bool | None = None,
+ can_manage_tags: bool | None = None,
**__pydantic_kwargs: Any,
) -> None:
# DO NOT EDIT MANUALLY!!!
@@ -103,5 +106,6 @@ class PromoteChatMember(TelegramMethod[bool]):
can_pin_messages=can_pin_messages,
can_manage_topics=can_manage_topics,
can_manage_direct_messages=can_manage_direct_messages,
+ can_manage_tags=can_manage_tags,
**__pydantic_kwargs,
)
diff --git a/aiogram/methods/send_checklist.py b/aiogram/methods/send_checklist.py
index 852c8110..7a00317b 100644
--- a/aiogram/methods/send_checklist.py
+++ b/aiogram/methods/send_checklist.py
@@ -31,7 +31,7 @@ class SendChecklist(TelegramMethod[Message]):
reply_parameters: ReplyParameters | None = None
"""A JSON-serialized object for description of the message to reply to"""
reply_markup: InlineKeyboardMarkup | None = None
- """A JSON-serialized object for an inline keyboard"""
+ """A JSON-serialized object for an `inline keyboard `_"""
if TYPE_CHECKING:
# DO NOT EDIT MANUALLY!!!
diff --git a/aiogram/methods/send_message_draft.py b/aiogram/methods/send_message_draft.py
index 6124f73e..b93c286c 100644
--- a/aiogram/methods/send_message_draft.py
+++ b/aiogram/methods/send_message_draft.py
@@ -8,7 +8,7 @@ from .base import TelegramMethod
class SendMessageDraft(TelegramMethod[bool]):
"""
- Use this method to stream a partial message to a user while the message is being generated; supported only for bots with forum topic mode enabled. Returns :code:`True` on success.
+ Use this method to stream a partial message to a user while the message is being generated. Returns :code:`True` on success.
Source: https://core.telegram.org/bots/api#sendmessagedraft
"""
diff --git a/aiogram/methods/set_chat_member_tag.py b/aiogram/methods/set_chat_member_tag.py
new file mode 100644
index 00000000..de8a2d09
--- /dev/null
+++ b/aiogram/methods/set_chat_member_tag.py
@@ -0,0 +1,40 @@
+from typing import TYPE_CHECKING, Any
+
+from ..types import ChatIdUnion
+from .base import TelegramMethod
+
+
+class SetChatMemberTag(TelegramMethod[bool]):
+ """
+ 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
+ """
+
+ __returning__ = bool
+ __api_method__ = "setChatMemberTag"
+
+ chat_id: ChatIdUnion
+ """Unique identifier for the target chat or username of the target supergroup (in the format :code:`@supergroupusername`)"""
+ user_id: int
+ """Unique identifier of the target user"""
+ tag: str | None = None
+ """New tag for the member; 0-16 characters, emoji are not allowed"""
+
+ if TYPE_CHECKING:
+ # DO NOT EDIT MANUALLY!!!
+ # This section was auto-generated via `butcher`
+
+ def __init__(
+ __pydantic__self__,
+ *,
+ chat_id: ChatIdUnion,
+ user_id: int,
+ tag: str | None = None,
+ **__pydantic_kwargs: Any,
+ ) -> None:
+ # DO NOT EDIT MANUALLY!!!
+ # This method was auto-generated via `butcher`
+ # Is needed only for type checking and IDE support without any additional plugins
+
+ super().__init__(chat_id=chat_id, user_id=user_id, tag=tag, **__pydantic_kwargs)
diff --git a/aiogram/types/base.py b/aiogram/types/base.py
index 7e47b564..4ba749d8 100644
--- a/aiogram/types/base.py
+++ b/aiogram/types/base.py
@@ -16,6 +16,7 @@ class TelegramObject(BotContextController, BaseModel):
populate_by_name=True,
arbitrary_types_allowed=True,
defer_build=True,
+ protected_namespaces=(),
)
@model_validator(mode="before")
diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py
index bea95afb..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,
@@ -1018,6 +1051,7 @@ class Chat(TelegramObject):
can_pin_messages: bool | None = None,
can_manage_topics: bool | None = None,
can_manage_direct_messages: bool | None = None,
+ can_manage_tags: bool | None = None,
**kwargs: Any,
) -> PromoteChatMember:
"""
@@ -1047,6 +1081,7 @@ class Chat(TelegramObject):
:param can_pin_messages: Pass :code:`True` if the administrator can pin messages; for supergroups only
:param can_manage_topics: Pass :code:`True` if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only
:param can_manage_direct_messages: Pass :code:`True` if the administrator can manage direct messages within the channel and decline suggested posts; for channels only
+ :param can_manage_tags: Pass :code:`True` if the administrator can edit the tags of regular members; for groups and supergroups only
:return: instance of method :class:`aiogram.methods.promote_chat_member.PromoteChatMember`
"""
# DO NOT EDIT MANUALLY!!!
@@ -1073,6 +1108,7 @@ class Chat(TelegramObject):
can_pin_messages=can_pin_messages,
can_manage_topics=can_manage_topics,
can_manage_direct_messages=can_manage_direct_messages,
+ can_manage_tags=can_manage_tags,
**kwargs,
).as_(self._bot)
diff --git a/aiogram/types/chat_administrator_rights.py b/aiogram/types/chat_administrator_rights.py
index cac9ae01..4c0811b9 100644
--- a/aiogram/types/chat_administrator_rights.py
+++ b/aiogram/types/chat_administrator_rights.py
@@ -47,6 +47,8 @@ class ChatAdministratorRights(TelegramObject):
"""*Optional*. :code:`True`, if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only"""
can_manage_direct_messages: bool | None = None
"""*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only"""
+ can_manage_tags: bool | None = None
+ """*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages."""
if TYPE_CHECKING:
# DO NOT EDIT MANUALLY!!!
@@ -71,6 +73,7 @@ class ChatAdministratorRights(TelegramObject):
can_pin_messages: bool | None = None,
can_manage_topics: bool | None = None,
can_manage_direct_messages: bool | None = None,
+ can_manage_tags: bool | None = None,
**__pydantic_kwargs: Any,
) -> None:
# DO NOT EDIT MANUALLY!!!
@@ -94,5 +97,6 @@ class ChatAdministratorRights(TelegramObject):
can_pin_messages=can_pin_messages,
can_manage_topics=can_manage_topics,
can_manage_direct_messages=can_manage_direct_messages,
+ can_manage_tags=can_manage_tags,
**__pydantic_kwargs,
)
diff --git a/aiogram/types/chat_member_administrator.py b/aiogram/types/chat_member_administrator.py
index b24dece3..1d8915bc 100644
--- a/aiogram/types/chat_member_administrator.py
+++ b/aiogram/types/chat_member_administrator.py
@@ -54,6 +54,8 @@ class ChatMemberAdministrator(ChatMember):
"""*Optional*. :code:`True`, if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only"""
can_manage_direct_messages: bool | None = None
"""*Optional*. :code:`True`, if the administrator can manage direct messages of the channel and decline suggested posts; for channels only"""
+ can_manage_tags: bool | None = None
+ """*Optional*. :code:`True`, if the administrator can edit the tags of regular members; for groups and supergroups only. If omitted defaults to the value of can_pin_messages."""
custom_title: str | None = None
"""*Optional*. Custom title for this user"""
@@ -83,6 +85,7 @@ class ChatMemberAdministrator(ChatMember):
can_pin_messages: bool | None = None,
can_manage_topics: bool | None = None,
can_manage_direct_messages: bool | None = None,
+ can_manage_tags: bool | None = None,
custom_title: str | None = None,
**__pydantic_kwargs: Any,
) -> None:
@@ -110,6 +113,7 @@ class ChatMemberAdministrator(ChatMember):
can_pin_messages=can_pin_messages,
can_manage_topics=can_manage_topics,
can_manage_direct_messages=can_manage_direct_messages,
+ can_manage_tags=can_manage_tags,
custom_title=custom_title,
**__pydantic_kwargs,
)
diff --git a/aiogram/types/chat_member_member.py b/aiogram/types/chat_member_member.py
index 5b3ce5e0..0cd89e76 100644
--- a/aiogram/types/chat_member_member.py
+++ b/aiogram/types/chat_member_member.py
@@ -21,6 +21,8 @@ class ChatMemberMember(ChatMember):
"""The member's status in the chat, always 'member'"""
user: User
"""Information about the user"""
+ tag: str | None = None
+ """*Optional*. Tag of the member"""
until_date: DateTime | None = None
"""*Optional*. Date when the user's subscription will expire; Unix time"""
@@ -33,6 +35,7 @@ class ChatMemberMember(ChatMember):
*,
status: Literal[ChatMemberStatus.MEMBER] = ChatMemberStatus.MEMBER,
user: User,
+ tag: str | None = None,
until_date: DateTime | None = None,
**__pydantic_kwargs: Any,
) -> None:
@@ -40,4 +43,6 @@ class ChatMemberMember(ChatMember):
# This method was auto-generated via `butcher`
# Is needed only for type checking and IDE support without any additional plugins
- super().__init__(status=status, user=user, until_date=until_date, **__pydantic_kwargs)
+ super().__init__(
+ status=status, user=user, tag=tag, until_date=until_date, **__pydantic_kwargs
+ )
diff --git a/aiogram/types/chat_member_restricted.py b/aiogram/types/chat_member_restricted.py
index 1466350f..0fc162ff 100644
--- a/aiogram/types/chat_member_restricted.py
+++ b/aiogram/types/chat_member_restricted.py
@@ -43,6 +43,8 @@ class ChatMemberRestricted(ChatMember):
""":code:`True`, if the user is allowed to send animations, games, stickers and use inline bots"""
can_add_web_page_previews: bool
""":code:`True`, if the user is allowed to add web page previews to their messages"""
+ can_edit_tag: bool
+ """:code:`True`, if the user is allowed to edit their own tag"""
can_change_info: bool
""":code:`True`, if the user is allowed to change the chat title, photo and other settings"""
can_invite_users: bool
@@ -53,6 +55,8 @@ class ChatMemberRestricted(ChatMember):
""":code:`True`, if the user is allowed to create forum topics"""
until_date: DateTime
"""Date when restrictions will be lifted for this user; Unix time. If 0, then the user is restricted forever"""
+ tag: str | None = None
+ """*Optional*. Tag of the member"""
if TYPE_CHECKING:
# DO NOT EDIT MANUALLY!!!
@@ -74,11 +78,13 @@ class ChatMemberRestricted(ChatMember):
can_send_polls: bool,
can_send_other_messages: bool,
can_add_web_page_previews: bool,
+ can_edit_tag: bool,
can_change_info: bool,
can_invite_users: bool,
can_pin_messages: bool,
can_manage_topics: bool,
until_date: DateTime,
+ tag: str | None = None,
**__pydantic_kwargs: Any,
) -> None:
# DO NOT EDIT MANUALLY!!!
@@ -99,10 +105,12 @@ class ChatMemberRestricted(ChatMember):
can_send_polls=can_send_polls,
can_send_other_messages=can_send_other_messages,
can_add_web_page_previews=can_add_web_page_previews,
+ can_edit_tag=can_edit_tag,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
can_manage_topics=can_manage_topics,
until_date=until_date,
+ tag=tag,
**__pydantic_kwargs,
)
diff --git a/aiogram/types/chat_permissions.py b/aiogram/types/chat_permissions.py
index dfb0fb77..473a38bb 100644
--- a/aiogram/types/chat_permissions.py
+++ b/aiogram/types/chat_permissions.py
@@ -32,6 +32,8 @@ class ChatPermissions(MutableTelegramObject):
"""*Optional*. :code:`True`, if the user is allowed to send animations, games, stickers and use inline bots"""
can_add_web_page_previews: bool | None = None
"""*Optional*. :code:`True`, if the user is allowed to add web page previews to their messages"""
+ can_edit_tag: bool | None = None
+ """*Optional*. :code:`True`, if the user is allowed to edit their own tag"""
can_change_info: bool | None = None
"""*Optional*. :code:`True`, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups"""
can_invite_users: bool | None = None
@@ -58,6 +60,7 @@ class ChatPermissions(MutableTelegramObject):
can_send_polls: bool | None = None,
can_send_other_messages: bool | None = None,
can_add_web_page_previews: bool | None = None,
+ can_edit_tag: bool | None = None,
can_change_info: bool | None = None,
can_invite_users: bool | None = None,
can_pin_messages: bool | None = None,
@@ -79,6 +82,7 @@ class ChatPermissions(MutableTelegramObject):
can_send_polls=can_send_polls,
can_send_other_messages=can_send_other_messages,
can_add_web_page_previews=can_add_web_page_previews,
+ can_edit_tag=can_edit_tag,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
diff --git a/aiogram/types/game_high_score.py b/aiogram/types/game_high_score.py
index e8fdcf40..5364be6e 100644
--- a/aiogram/types/game_high_score.py
+++ b/aiogram/types/game_high_score.py
@@ -15,8 +15,6 @@ class GameHighScore(TelegramObject):
If you've got any questions, please check out our `https://core.telegram.org/bots/faq `_ **Bot FAQ »**
- -
-
Source: https://core.telegram.org/bots/api#gamehighscore
"""
diff --git a/aiogram/types/inline_query_result_document.py b/aiogram/types/inline_query_result_document.py
index c40d31b8..1665c8b3 100644
--- a/aiogram/types/inline_query_result_document.py
+++ b/aiogram/types/inline_query_result_document.py
@@ -38,7 +38,7 @@ class InlineQueryResultDocument(InlineQueryResult):
description: str | None = None
"""*Optional*. Short description of the result"""
reply_markup: InlineKeyboardMarkup | None = None
- """*Optional*. Inline keyboard attached to the message"""
+ """*Optional*. `Inline keyboard `_ attached to the message"""
input_message_content: InputMessageContentUnion | None = None
"""*Optional*. Content of the message to be sent instead of the file"""
thumbnail_url: str | None = None
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index b35fe0fb..1b7f9677 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -157,6 +157,8 @@ class Message(MaybeInaccessibleMessage):
"""*Optional*. If the sender of the message boosted the chat, the number of boosts added by the user"""
sender_business_bot: User | None = None
"""*Optional*. The bot that actually sent the message on behalf of the business account. Available only for outgoing messages sent on behalf of the connected business account."""
+ sender_tag: str | None = None
+ """*Optional*. Tag or custom title of the sender of the message; for supergroups only"""
business_connection_id: str | None = None
"""*Optional*. Unique identifier of the business connection from which the message was received. If non-empty, the message belongs to a chat of the corresponding business account that is independent from any potential bot chat which might share the same identifier."""
forward_origin: MessageOriginUnion | None = None
@@ -186,7 +188,7 @@ class Message(MaybeInaccessibleMessage):
is_paid_post: bool | None = None
"""*Optional*. :code:`True`, if the message is a paid post. Note that such posts must not be deleted for 24 hours to receive the payment and can't be edited."""
media_group_id: str | None = None
- """*Optional*. The unique identifier of a media message group this message belongs to"""
+ """*Optional*. The unique identifier inside this chat of a media message group this message belongs to"""
author_signature: str | None = None
"""*Optional*. Signature of the post author for messages in channels, or the custom title of an anonymous group administrator"""
paid_star_count: int | None = None
@@ -348,7 +350,7 @@ class Message(MaybeInaccessibleMessage):
web_app_data: WebAppData | None = None
"""*Optional*. Service message: data sent by a Web App"""
reply_markup: InlineKeyboardMarkup | None = None
- """*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons."""
+ """*Optional*. `Inline keyboard `_ attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons."""
forward_date: DateTime | None = Field(None, json_schema_extra={"deprecated": True})
"""*Optional*. For forwarded messages, date the original message was sent in Unix time
@@ -401,6 +403,7 @@ class Message(MaybeInaccessibleMessage):
sender_chat: Chat | None = None,
sender_boost_count: int | None = None,
sender_business_bot: User | None = None,
+ sender_tag: str | None = None,
business_connection_id: str | None = None,
forward_origin: MessageOriginUnion | None = None,
is_topic_message: bool | None = None,
@@ -520,6 +523,7 @@ class Message(MaybeInaccessibleMessage):
sender_chat=sender_chat,
sender_boost_count=sender_boost_count,
sender_business_bot=sender_business_bot,
+ sender_tag=sender_tag,
business_connection_id=business_connection_id,
forward_origin=forward_origin,
is_topic_message=is_topic_message,
diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py
index 608b8435..68ae3025 100644
--- a/aiogram/types/message_entity.py
+++ b/aiogram/types/message_entity.py
@@ -17,7 +17,7 @@ class MessageEntity(MutableTelegramObject):
"""
type: str
- """Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers)"""
+ """Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag` or :code:`#hashtag@chatusername`), 'cashtag' (:code:`$USD` or :code:`$USD@chatusername`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'blockquote' (block quotation), 'expandable_blockquote' (collapsed-by-default block quotation), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames `_), 'custom_emoji' (for inline custom emoji stickers), or 'date_time' (for formatted date and time)"""
offset: int
"""Offset in `UTF-16 code units `_ to the start of the entity"""
length: int
@@ -30,6 +30,10 @@ class MessageEntity(MutableTelegramObject):
"""*Optional*. For 'pre' only, the programming language of the entity text"""
custom_emoji_id: str | None = None
"""*Optional*. For 'custom_emoji' only, unique identifier of the custom emoji. Use :class:`aiogram.methods.get_custom_emoji_stickers.GetCustomEmojiStickers` to get full information about the sticker"""
+ unix_time: int | None = None
+ """*Optional*. For 'date_time' only, the Unix time associated with the entity"""
+ date_time_format: str | None = None
+ """*Optional*. For 'date_time' only, the string that defines the formatting of the date and time. See `date-time entity formatting `_ for more details."""
if TYPE_CHECKING:
# DO NOT EDIT MANUALLY!!!
@@ -45,6 +49,8 @@ class MessageEntity(MutableTelegramObject):
user: User | None = None,
language: str | None = None,
custom_emoji_id: str | None = None,
+ unix_time: int | None = None,
+ date_time_format: str | None = None,
**__pydantic_kwargs: Any,
) -> None:
# DO NOT EDIT MANUALLY!!!
@@ -59,6 +65,8 @@ class MessageEntity(MutableTelegramObject):
user=user,
language=language,
custom_emoji_id=custom_emoji_id,
+ unix_time=unix_time,
+ date_time_format=date_time_format,
**__pydantic_kwargs,
)
diff --git a/aiogram/utils/formatting.py b/aiogram/utils/formatting.py
index cbfd9758..4dfccd64 100644
--- a/aiogram/utils/formatting.py
+++ b/aiogram/utils/formatting.py
@@ -2,6 +2,7 @@ from __future__ import annotations
import textwrap
from collections.abc import Generator, Iterable, Iterator
+from datetime import datetime
from typing import Any, ClassVar
from typing_extensions import Self
@@ -534,6 +535,26 @@ class ExpandableBlockQuote(Text):
type = MessageEntityType.EXPANDABLE_BLOCKQUOTE
+class DateTime(Text):
+ type = MessageEntityType.DATE_TIME
+
+ def __init__(
+ self,
+ *body: NodeType,
+ unix_time: int | datetime,
+ date_time_format: str | None = None,
+ **params: Any,
+ ) -> None:
+ if isinstance(unix_time, datetime):
+ unix_time = int(unix_time.timestamp())
+ super().__init__(
+ *body,
+ unix_time=unix_time,
+ date_time_format=date_time_format,
+ **params,
+ )
+
+
NODE_TYPES: dict[str | None, type[Text]] = {
Text.type: Text,
HashTag.type: HashTag,
@@ -554,6 +575,7 @@ NODE_TYPES: dict[str | None, type[Text]] = {
CustomEmoji.type: CustomEmoji,
BlockQuote.type: BlockQuote,
ExpandableBlockQuote.type: ExpandableBlockQuote,
+ DateTime.type: DateTime,
}
diff --git a/aiogram/utils/keyboard.py b/aiogram/utils/keyboard.py
index 8824edfd..2dba452c 100644
--- a/aiogram/utils/keyboard.py
+++ b/aiogram/utils/keyboard.py
@@ -303,6 +303,8 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]):
self,
*,
text: str,
+ icon_custom_emoji_id: str | None = None,
+ style: str | None = None,
url: str | None = None,
callback_data: str | CallbackData | None = None,
web_app: WebAppInfo | None = None,
@@ -319,6 +321,8 @@ class InlineKeyboardBuilder(KeyboardBuilder[InlineKeyboardButton]):
InlineKeyboardBuilder,
self._button(
text=text,
+ icon_custom_emoji_id=icon_custom_emoji_id,
+ style=style,
url=url,
callback_data=callback_data,
web_app=web_app,
@@ -375,6 +379,8 @@ class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]):
self,
*,
text: str,
+ icon_custom_emoji_id: str | None = None,
+ style: str | None = None,
request_users: KeyboardButtonRequestUsers | None = None,
request_chat: KeyboardButtonRequestChat | None = None,
request_contact: bool | None = None,
@@ -387,6 +393,8 @@ class ReplyKeyboardBuilder(KeyboardBuilder[KeyboardButton]):
ReplyKeyboardBuilder,
self._button(
text=text,
+ icon_custom_emoji_id=icon_custom_emoji_id,
+ style=style,
request_users=request_users,
request_chat=request_chat,
request_contact=request_contact,
diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py
index 00762c0a..b8edead2 100644
--- a/aiogram/utils/text_decorations.py
+++ b/aiogram/utils/text_decorations.py
@@ -3,9 +3,11 @@ from __future__ import annotations
import html
import re
from abc import ABC, abstractmethod
+from datetime import date, datetime, time
from typing import TYPE_CHECKING, cast
from aiogram.enums import MessageEntityType
+from aiogram.utils.link import create_tg_link
if TYPE_CHECKING:
from collections.abc import Generator
@@ -78,6 +80,12 @@ class TextDecoration(ABC):
return self.link(value=text, link=cast(str, entity.url))
if entity.type == MessageEntityType.CUSTOM_EMOJI:
return self.custom_emoji(value=text, custom_emoji_id=cast(str, entity.custom_emoji_id))
+ if entity.type == MessageEntityType.DATE_TIME:
+ return self.date_time(
+ value=text,
+ unix_time=cast(int, entity.unix_time),
+ date_time_format=entity.date_time_format,
+ )
# This case is not possible because of `if` above, but if any new entity is added to
# API it will be here too
@@ -180,54 +188,105 @@ class TextDecoration(ABC):
def expandable_blockquote(self, value: str) -> str:
pass
+ @abstractmethod
+ def date_time(
+ self,
+ value: str,
+ unix_time: int | datetime,
+ date_time_format: str | None = None,
+ ) -> str:
+ pass
+
class HtmlDecoration(TextDecoration):
BOLD_TAG = "b"
ITALIC_TAG = "i"
UNDERLINE_TAG = "u"
STRIKETHROUGH_TAG = "s"
+ CODE_TAG = "code"
+ PRE_TAG = "pre"
+ LINK_TAG = "a"
SPOILER_TAG = "tg-spoiler"
EMOJI_TAG = "tg-emoji"
+ DATE_TIME_TAG = "tg-time"
BLOCKQUOTE_TAG = "blockquote"
+ def _tag(
+ self,
+ tag: str,
+ content: str,
+ *,
+ attrs: dict[str, str] | None = None,
+ flags: list[str] | None = None,
+ ) -> str:
+ prepared_attrs: list[str] = []
+ if attrs:
+ prepared_attrs.extend(f'{k}="{v}"' for k, v in attrs.items())
+ if flags:
+ prepared_attrs.extend(f"{flag}" for flag in flags)
+
+ attrs_str = " ".join(prepared_attrs)
+ if attrs_str:
+ attrs_str = " " + attrs_str
+
+ return f"<{tag}{attrs_str}>{content}{tag}>"
+
def link(self, value: str, link: str) -> str:
- return f'{value}'
+ return self._tag(self.LINK_TAG, value, attrs={"href": link})
def bold(self, value: str) -> str:
- return f"<{self.BOLD_TAG}>{value}{self.BOLD_TAG}>"
+ return self._tag(self.BOLD_TAG, value)
def italic(self, value: str) -> str:
- return f"<{self.ITALIC_TAG}>{value}{self.ITALIC_TAG}>"
+ return self._tag(self.ITALIC_TAG, value)
def code(self, value: str) -> str:
- return f"{value}"
+ return self._tag(self.CODE_TAG, value)
def pre(self, value: str) -> str:
- return f"{value}"
+ return self._tag(self.PRE_TAG, value)
def pre_language(self, value: str, language: str) -> str:
- return f'{value}
'
+ return self._tag(
+ self.PRE_TAG,
+ self._tag(self.CODE_TAG, value, attrs={"language": f"language-{language}"}),
+ )
def underline(self, value: str) -> str:
- return f"<{self.UNDERLINE_TAG}>{value}{self.UNDERLINE_TAG}>"
+ return self._tag(self.UNDERLINE_TAG, value)
def strikethrough(self, value: str) -> str:
- return f"<{self.STRIKETHROUGH_TAG}>{value}{self.STRIKETHROUGH_TAG}>"
+ return self._tag(self.STRIKETHROUGH_TAG, value)
def spoiler(self, value: str) -> str:
- return f"<{self.SPOILER_TAG}>{value}{self.SPOILER_TAG}>"
+ return self._tag(self.SPOILER_TAG, value)
def quote(self, value: str) -> str:
return html.escape(value, quote=False)
def custom_emoji(self, value: str, custom_emoji_id: str) -> str:
- return f'<{self.EMOJI_TAG} emoji-id="{custom_emoji_id}">{value}{self.EMOJI_TAG}>'
+ return self._tag(self.EMOJI_TAG, value, attrs={"emoji_id": custom_emoji_id})
def blockquote(self, value: str) -> str:
- return f"<{self.BLOCKQUOTE_TAG}>{value}{self.BLOCKQUOTE_TAG}>"
+ return self._tag(self.BLOCKQUOTE_TAG, value)
def expandable_blockquote(self, value: str) -> str:
- return f"<{self.BLOCKQUOTE_TAG} expandable>{value}{self.BLOCKQUOTE_TAG}>"
+ return self._tag(self.BLOCKQUOTE_TAG, value, flags=["expandable"])
+
+ def date_time(
+ self,
+ value: str,
+ unix_time: int | datetime,
+ date_time_format: str | None = None,
+ ) -> str:
+ if isinstance(unix_time, datetime):
+ unix_time = int(unix_time.timestamp())
+
+ args = {"unix": str(unix_time)}
+ if date_time_format:
+ args["format"] = date_time_format
+
+ return self._tag(self.DATE_TIME_TAG, value, attrs=args)
class MarkdownDecoration(TextDecoration):
@@ -264,7 +323,8 @@ class MarkdownDecoration(TextDecoration):
return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value)
def custom_emoji(self, value: str, custom_emoji_id: str) -> str:
- return f"!{self.link(value=value, link=f'tg://emoji?id={custom_emoji_id}')}"
+ link = create_tg_link("emoji", emoji_id=custom_emoji_id)
+ return f"!{self.link(value=value, link=link)}"
def blockquote(self, value: str) -> str:
return "\n".join(f">{line}" for line in value.splitlines())
@@ -272,6 +332,22 @@ class MarkdownDecoration(TextDecoration):
def expandable_blockquote(self, value: str) -> str:
return "\n".join(f">{line}" for line in value.splitlines()) + "||"
+ def date_time(
+ self,
+ value: str,
+ unix_time: int | datetime,
+ date_time_format: str | None = None,
+ ) -> str:
+ if isinstance(unix_time, datetime):
+ unix_time = int(unix_time.timestamp())
+
+ link_params = {"unix": str(unix_time)}
+ if date_time_format:
+ link_params["format"] = date_time_format
+ link = create_tg_link("time", **link_params)
+
+ return f"!{self.link(value, link=link)}"
+
html_decoration = HtmlDecoration()
markdown_decoration = MarkdownDecoration()
diff --git a/docs/api/methods/get_user_profile_audios.rst b/docs/api/methods/get_user_profile_audios.rst
index f9567228..553ab296 100644
--- a/docs/api/methods/get_user_profile_audios.rst
+++ b/docs/api/methods/get_user_profile_audios.rst
@@ -36,3 +36,11 @@ With specific bot
.. code-block:: python
result: UserProfileAudios = await bot(GetUserProfileAudios(...))
+
+
+
+
+As shortcut from received object
+--------------------------------
+
+- :meth:`aiogram.types.user.User.get_profile_audios`
diff --git a/docs/api/methods/index.rst b/docs/api/methods/index.rst
index bbd6303a..f83a4f10 100644
--- a/docs/api/methods/index.rst
+++ b/docs/api/methods/index.rst
@@ -127,6 +127,7 @@ Available methods
set_business_account_username
set_chat_administrator_custom_title
set_chat_description
+ set_chat_member_tag
set_chat_menu_button
set_chat_permissions
set_chat_photo
diff --git a/docs/api/methods/set_chat_member_tag.rst b/docs/api/methods/set_chat_member_tag.rst
new file mode 100644
index 00000000..79090f6f
--- /dev/null
+++ b/docs/api/methods/set_chat_member_tag.rst
@@ -0,0 +1,45 @@
+################
+setChatMemberTag
+################
+
+Returns: :obj:`bool`
+
+.. automodule:: aiogram.methods.set_chat_member_tag
+ :members:
+ :member-order: bysource
+ :undoc-members: True
+ :exclude-members: model_config,model_fields
+
+
+Usage
+=====
+
+As bot method
+-------------
+
+.. code-block::
+
+ result: bool = await bot.set_chat_member_tag(...)
+
+
+Method as object
+----------------
+
+Imports:
+
+- :code:`from aiogram.methods.set_chat_member_tag import SetChatMemberTag`
+- alias: :code:`from aiogram.methods import SetChatMemberTag`
+
+With specific bot
+~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ result: bool = await bot(SetChatMemberTag(...))
+
+As reply into Webhook in handler
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ return SetChatMemberTag(...)
diff --git a/docs/dispatcher/webhook.rst b/docs/dispatcher/webhook.rst
index 73bcddbc..26c58f36 100644
--- a/docs/dispatcher/webhook.rst
+++ b/docs/dispatcher/webhook.rst
@@ -66,6 +66,19 @@ It can be acy using firewall rules or nginx configuration or middleware on appli
So, aiogram has an implementation of the IP filtering middleware for aiohttp.
+`aiogram` IP filtering middleware reads the left-most IP address from `X-Forwarded-For`.
+
+.. warning::
+
+ `X-Forwarded-For` is trustworthy only if all webhook traffic goes through a trusted reverse proxy that rewrites this header.
+ If your application is directly reachable from the Internet, this header can be forged.
+
+For production deployments, use defense in depth:
+
+- Always set and verify :code:`X-Telegram-Bot-Api-Secret-Token`
+- Restrict network access to the webhook endpoint (firewall, security groups, ACL)
+- Ensure the backend app is not publicly reachable and accepts requests only from the trusted proxy
+
.. autofunction:: aiogram.webhook.aiohttp_server.ip_filter_middleware
.. autoclass:: aiogram.webhook.security.IPFilter
diff --git a/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po b/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po
index 880fef46..ee10e8ca 100644
--- a/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po
+++ b/docs/locale/uk_UA/LC_MESSAGES/dispatcher/webhook.po
@@ -203,45 +203,95 @@ msgstr ""
#: ../../dispatcher/webhook.rst:51
msgid "Security"
-msgstr ""
+msgstr "Безпека"
#: ../../dispatcher/webhook.rst:53
msgid ""
"Telegram supports two methods to verify incoming requests that they are "
"from Telegram:"
-msgstr ""
+msgstr "Telegram підтримує два методи перевірки вхідних запитів, що вони надходять від Telegram:"
#: ../../dispatcher/webhook.rst:56
msgid "Using a secret token"
-msgstr ""
+msgstr "Використання секретного токена"
#: ../../dispatcher/webhook.rst:58
msgid ""
"When you set webhook, you can specify a secret token and then use it to "
"verify incoming requests."
msgstr ""
+"Коли ви налаштовуєте webhook, ви можете вказати секретний токен і потім "
+"використовувати його для перевірки вхідних запитів."
#: ../../dispatcher/webhook.rst:61
msgid "Using IP filtering"
-msgstr ""
+msgstr "Використання фільтрації за IP"
#: ../../dispatcher/webhook.rst:63
msgid ""
"You can specify a list of IP addresses from which you expect incoming "
"requests, and then use it to verify incoming requests."
msgstr ""
+"Ви можете вказати список IP-адрес, з яких очікуєте вхідні запити, і "
+"використовувати його для перевірки запитів."
#: ../../dispatcher/webhook.rst:65
msgid ""
"It can be acy using firewall rules or nginx configuration or middleware "
"on application level."
msgstr ""
+"Це можна зробити за допомогою правил firewall, конфігурації nginx або "
+"middleware на рівні застосунку."
#: ../../dispatcher/webhook.rst:67
msgid ""
"So, aiogram has an implementation of the IP filtering middleware for "
"aiohttp."
msgstr ""
+"Тому в aiogram є реалізація middleware для фільтрації за IP для aiohttp."
+
+#: ../../dispatcher/webhook.rst:69
+msgid ""
+"`aiogram` IP filtering middleware reads the left-most IP address from "
+"`X-Forwarded-For`."
+msgstr ""
+"IP-фільтр middleware в `aiogram` читає крайню ліву IP-адресу з "
+"`X-Forwarded-For`."
+
+#: ../../dispatcher/webhook.rst:73
+msgid ""
+"`X-Forwarded-For` is trustworthy only if all webhook traffic goes through a"
+" trusted reverse proxy that rewrites this header. If your application is "
+"directly reachable from the Internet, this header can be forged."
+msgstr ""
+"`X-Forwarded-For` можна вважати надійним лише тоді, коли весь webhook-"
+"трафік проходить через довірений reverse proxy, який перезаписує цей "
+"заголовок. Якщо ваш застосунок напряму доступний з Інтернету, цей "
+"заголовок можна підробити."
+
+#: ../../dispatcher/webhook.rst:76
+msgid "For production deployments, use defense in depth:"
+msgstr "Для production-деплойментів використовуйте багаторівневий захист:"
+
+#: ../../dispatcher/webhook.rst:78
+msgid "Always set and verify :code:`X-Telegram-Bot-Api-Secret-Token`"
+msgstr "Завжди встановлюйте та перевіряйте :code:`X-Telegram-Bot-Api-Secret-Token`"
+
+#: ../../dispatcher/webhook.rst:79
+msgid ""
+"Restrict network access to the webhook endpoint (firewall, security "
+"groups, ACL)"
+msgstr ""
+"Обмежуйте мережевий доступ до webhook endpoint (firewall, security groups, "
+"ACL)"
+
+#: ../../dispatcher/webhook.rst:80
+msgid ""
+"Ensure the backend app is not publicly reachable and accepts requests only "
+"from the trusted proxy"
+msgstr ""
+"Переконайтеся, що backend-застосунок не доступний публічно та приймає "
+"запити лише від довіреного proxy"
#: ../../dispatcher/webhook.rst:75
msgid "Examples"
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_fsm/test_scene.py b/tests/test_fsm/test_scene.py
index 3a9944b0..ba601325 100644
--- a/tests/test_fsm/test_scene.py
+++ b/tests/test_fsm/test_scene.py
@@ -253,6 +253,7 @@ class TestSceneHandlerWrapper:
state_mock = AsyncMock(spec=FSMContext)
scenes_mock = AsyncMock(spec=ScenesManager)
+ scenes_mock.data = {}
event_update_mock = Update(
update_id=42,
message=Message(
@@ -282,6 +283,7 @@ class TestSceneHandlerWrapper:
state_mock = AsyncMock(spec=FSMContext)
scenes_mock = AsyncMock(spec=ScenesManager)
+ scenes_mock.data = {}
event_update_mock = Update(
update_id=42,
message=Message(
diff --git a/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py b/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py
new file mode 100644
index 00000000..d0433298
--- /dev/null
+++ b/tests/test_issues/test_1687_scene_goto_loses_middleware_data.py
@@ -0,0 +1,106 @@
+from collections.abc import Awaitable, Callable
+from datetime import datetime
+from typing import Any
+
+from aiogram import BaseMiddleware, Dispatcher
+from aiogram.enums import ChatType
+from aiogram.filters import CommandStart
+from aiogram.fsm.scene import After, Scene, SceneRegistry, on
+from aiogram.types import Chat, Message, TelegramObject, Update, User
+from tests.mocked_bot import MockedBot
+
+
+class TestContextMiddleware(BaseMiddleware):
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: dict[str, Any],
+ ) -> Any:
+ data["test_context"] = "context from middleware"
+ return await handler(event, data)
+
+
+class TargetScene(Scene, state="target"):
+ entered_with_context: str | None = None
+
+ @on.message.enter()
+ async def on_enter(self, message: Message, test_context: str) -> None:
+ type(self).entered_with_context = test_context
+
+
+class StartScene(Scene, state="start"):
+ @on.message.enter()
+ async def on_start(self, message: Message) -> None:
+ await self.wizard.goto(TargetScene)
+
+
+class StartSceneWithAfter(Scene, state="start_with_after"):
+ @on.message(after=After.goto(TargetScene))
+ async def goto_target_with_after(self, message: Message) -> None:
+ pass
+
+
+async def test_scene_goto_preserves_message_middleware_data(bot: MockedBot) -> None:
+ dp = Dispatcher()
+ registry = SceneRegistry(dp)
+ registry.add(StartScene, TargetScene)
+ dp.message.register(StartScene.as_handler(), CommandStart())
+ dp.message.middleware(TestContextMiddleware())
+
+ TargetScene.entered_with_context = None
+
+ update = Update(
+ update_id=1,
+ message=Message(
+ message_id=1,
+ date=datetime.now(),
+ chat=Chat(id=42, type=ChatType.PRIVATE),
+ from_user=User(id=42, is_bot=False, first_name="Test"),
+ text="/start",
+ ),
+ )
+
+ await dp.feed_update(bot, update)
+
+ assert TargetScene.entered_with_context == "context from middleware"
+
+
+async def test_scene_after_goto_preserves_message_middleware_data(bot: MockedBot) -> None:
+ dp = Dispatcher()
+ registry = SceneRegistry(dp)
+ registry.add(StartSceneWithAfter, TargetScene)
+ dp.message.register(StartSceneWithAfter.as_handler(), CommandStart())
+ dp.message.middleware(TestContextMiddleware())
+
+ TargetScene.entered_with_context = None
+
+ await dp.feed_update(
+ bot,
+ Update(
+ update_id=1,
+ message=Message(
+ message_id=1,
+ date=datetime.now(),
+ chat=Chat(id=42, type=ChatType.PRIVATE),
+ from_user=User(id=42, is_bot=False, first_name="Test"),
+ text="/start",
+ ),
+ ),
+ )
+
+ await dp.feed_update(
+ bot,
+ Update(
+ update_id=2,
+ message=Message(
+ message_id=2,
+ date=datetime.now(),
+ chat=Chat(id=42, type=ChatType.PRIVATE),
+ from_user=User(id=42, is_bot=False, first_name="Test"),
+ text="go",
+ ),
+ ),
+ )
+
+ assert TargetScene.entered_with_context == "context from middleware"
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..8efc1598 100644
--- a/tests/test_utils/test_formatting.py
+++ b/tests/test_utils/test_formatting.py
@@ -1,3 +1,5 @@
+from datetime import datetime, timezone
+
import pytest
from aiogram.enums import MessageEntityType
@@ -9,6 +11,7 @@ from aiogram.utils.formatting import (
CashTag,
Code,
CustomEmoji,
+ DateTime,
Email,
ExpandableBlockQuote,
HashTag,
@@ -93,7 +96,7 @@ class TestNode:
],
[
Pre("test", language="python"),
- 'test
',
+ 'test
',
],
[
TextLink("test", url="https://example.com"),
@@ -105,7 +108,7 @@ class TestNode:
],
[
CustomEmoji("test", custom_emoji_id="42"),
- 'test',
+ 'test',
],
[
BlockQuote("test"),
@@ -115,6 +118,14 @@ class TestNode:
ExpandableBlockQuote("test"),
"test
",
],
+ [
+ DateTime("test", unix_time=42, date_time_format="yMd"),
+ 'test',
+ ],
+ [
+ DateTime("test", unix_time=42),
+ 'test',
+ ],
],
)
def test_render_plain_only(self, node: Text, result: str):
@@ -358,6 +369,38 @@ 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_date_time_with_datetime_object(self):
+ dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
+ node = DateTime("test", unix_time=dt)
+ assert isinstance(node, DateTime)
+ assert node._params["unix_time"] == 1704067200
+
+ def test_date_time_with_datetime_and_format(self):
+ dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
+ node = DateTime("test", unix_time=dt, date_time_format="yMd")
+ assert node._params["unix_time"] == 1704067200
+ assert node._params["date_time_format"] == "yMd"
+
+ def test_date_time_as_markdown(self):
+ node = DateTime("test", unix_time=42, date_time_format="yMd")
+ assert node.as_markdown() == ""
+
def test_as_line(self):
node = as_line("test", "test", "test")
assert isinstance(node, Text)
diff --git a/tests/test_utils/test_keyboard.py b/tests/test_utils/test_keyboard.py
index e80fed8e..b374e7ac 100644
--- a/tests/test_utils/test_keyboard.py
+++ b/tests/test_utils/test_keyboard.py
@@ -214,11 +214,63 @@ class TestKeyboardBuilder:
"builder_type,kwargs,expected",
[
[ReplyKeyboardBuilder, {"text": "test"}, KeyboardButton(text="test")],
+ [
+ ReplyKeyboardBuilder,
+ {"text": "test", "icon_custom_emoji_id": "emoji-id"},
+ KeyboardButton(text="test", icon_custom_emoji_id="emoji-id"),
+ ],
+ [
+ ReplyKeyboardBuilder,
+ {"text": "test", "style": "success"},
+ KeyboardButton(text="test", style="success"),
+ ],
+ [
+ ReplyKeyboardBuilder,
+ {"text": "test", "icon_custom_emoji_id": "emoji-id", "style": "success"},
+ KeyboardButton(
+ text="test",
+ icon_custom_emoji_id="emoji-id",
+ style="success",
+ ),
+ ],
[
InlineKeyboardBuilder,
{"text": "test", "callback_data": "callback"},
InlineKeyboardButton(text="test", callback_data="callback"),
],
+ [
+ InlineKeyboardBuilder,
+ {
+ "text": "test",
+ "icon_custom_emoji_id": "emoji-id",
+ "callback_data": "callback",
+ },
+ InlineKeyboardButton(
+ text="test",
+ icon_custom_emoji_id="emoji-id",
+ callback_data="callback",
+ ),
+ ],
+ [
+ InlineKeyboardBuilder,
+ {"text": "test", "style": "primary", "callback_data": "callback"},
+ InlineKeyboardButton(text="test", style="primary", callback_data="callback"),
+ ],
+ [
+ InlineKeyboardBuilder,
+ {
+ "text": "test",
+ "icon_custom_emoji_id": "emoji-id",
+ "style": "primary",
+ "callback_data": "callback",
+ },
+ InlineKeyboardButton(
+ text="test",
+ icon_custom_emoji_id="emoji-id",
+ style="primary",
+ callback_data="callback",
+ ),
+ ],
[
InlineKeyboardBuilder,
{"text": "test", "callback_data": MyCallback(value="test")},
@@ -242,6 +294,45 @@ class TestKeyboardBuilder:
def test_as_markup(self, builder, expected):
assert isinstance(builder.as_markup(), expected)
+ @pytest.mark.parametrize(
+ "builder,button_kwargs,icon_custom_emoji_id,style",
+ [
+ [
+ ReplyKeyboardBuilder(),
+ {"text": "test", "icon_custom_emoji_id": "emoji-id", "style": "success"},
+ "emoji-id",
+ "success",
+ ],
+ [
+ InlineKeyboardBuilder(),
+ {
+ "text": "test",
+ "icon_custom_emoji_id": "emoji-id",
+ "style": "primary",
+ "callback_data": "callback",
+ },
+ "emoji-id",
+ "primary",
+ ],
+ ],
+ )
+ def test_as_markup_preserves_icon_and_style(
+ self,
+ builder,
+ button_kwargs,
+ icon_custom_emoji_id,
+ style,
+ ):
+ builder.button(**button_kwargs)
+ markup = builder.as_markup()
+ if isinstance(markup, ReplyKeyboardMarkup):
+ button = markup.keyboard[0][0]
+ else:
+ button = markup.inline_keyboard[0][0]
+
+ assert button.icon_custom_emoji_id == icon_custom_emoji_id
+ assert button.style == style
+
@pytest.mark.parametrize(
"markup,builder_type",
[
diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py
index b4ccb5e8..6325edb8 100644
--- a/tests/test_utils/test_text_decorations.py
+++ b/tests/test_utils/test_text_decorations.py
@@ -1,3 +1,5 @@
+from datetime import datetime, timezone
+
import pytest
from aiogram.types import MessageEntity, User
@@ -25,7 +27,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 +59,7 @@ class TestTextDecoration:
[
html_decoration,
MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"),
- 'test',
+ 'test',
],
[
html_decoration,
@@ -74,6 +76,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 +115,7 @@ class TestTextDecoration:
[
markdown_decoration,
MessageEntity(type="custom_emoji", offset=0, length=5, custom_emoji_id="42"),
- "",
+ "",
],
[
markdown_decoration,
@@ -124,6 +137,27 @@ 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",
+ ),
+ "",
+ ],
+ [
+ html_decoration,
+ MessageEntity(type="date_time", offset=0, length=5, unix_time=42),
+ 'test',
+ ],
+ [
+ markdown_decoration,
+ MessageEntity(type="date_time", offset=0, length=5, unix_time=42),
+ "",
+ ],
],
)
def test_apply_single_entity(
@@ -131,6 +165,38 @@ class TestTextDecoration:
):
assert decorator.apply_entity(entity, "test") == result
+ @pytest.mark.parametrize(
+ "decorator,date_time_format,expected",
+ [
+ (
+ html_decoration,
+ None,
+ 'test',
+ ),
+ (
+ html_decoration,
+ "yMd",
+ 'test',
+ ),
+ (
+ markdown_decoration,
+ None,
+ "",
+ ),
+ (
+ markdown_decoration,
+ "yMd",
+ "",
+ ),
+ ],
+ )
+ def test_date_time_with_datetime_object(
+ self, decorator: TextDecoration, date_time_format: str | None, expected: str
+ ):
+ dt = datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
+ result = decorator.date_time("test", unix_time=dt, date_time_format=date_time_format)
+ assert result == expected
+
def test_unknown_apply_entity(self):
assert (
html_decoration.apply_entity(
@@ -296,6 +362,22 @@ class TestTextDecoration:
],
"test@example.com",
],
+ [
+ html_decoration,
+ "test",
+ [MessageEntity(type="date_time", offset=0, length=4, unix_time=42)],
+ 'test',
+ ],
+ [
+ html_decoration,
+ "test",
+ [
+ MessageEntity(
+ type="date_time", offset=0, length=4, unix_time=42, date_time_format="yMd"
+ )
+ ],
+ 'test',
+ ],
],
)
def test_unparse(