From 27dea25c5285e34efc873b8d548bb83f4beee902 Mon Sep 17 00:00:00 2001 From: KatantDev Date: Sun, 6 Nov 2022 08:51:42 +1000 Subject: [PATCH] Added support for topics in Bot API 6.3 (#1061) * Added support for topics in Bot API 6.3 * Added the field can_manage_topics * Added new classes for topics * Added is_forum field to Chat class Co-authored-by: katant --- aiogram/bot/bot.py | 96 +++++++++++++++++++++- aiogram/dispatcher/webhook.py | 7 +- aiogram/types/__init__.py | 6 ++ aiogram/types/chat.py | 3 + aiogram/types/chat_administrator_rights.py | 7 +- aiogram/types/chat_member.py | 3 + aiogram/types/chat_permissions.py | 3 + aiogram/types/forum_topic_closed.py | 10 +++ aiogram/types/forum_topic_created.py | 13 +++ aiogram/types/forum_topic_reopened.py | 10 +++ aiogram/types/message.py | 55 ++++++++++++- aiogram/types/video_chat_started.py | 2 +- tests/types/dataset.py | 1 + tests/types/test_chat_member.py | 3 + 14 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 aiogram/types/forum_topic_closed.py create mode 100644 aiogram/types/forum_topic_created.py create mode 100644 aiogram/types/forum_topic_reopened.py diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index a641d4e70c..cc839302ad 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -274,6 +274,7 @@ async def send_message(self, parse_mode: typing.Optional[base.String] = None, entities: typing.Optional[typing.List[types.MessageEntity]] = None, disable_web_page_preview: typing.Optional[base.Boolean] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -291,6 +292,10 @@ async def send_message(self, :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param text: Text of the message to be sent :type text: :obj:`base.String` @@ -345,6 +350,7 @@ async def forward_message(self, chat_id: typing.Union[base.Integer, base.String], from_chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, ) -> types.Message: @@ -357,6 +363,11 @@ async def forward_message(self, username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + + :param from_chat_id: Unique identifier for the chat where the original message was sent :type from_chat_id: :obj:`typing.Union[base.Integer, base.String]` @@ -390,6 +401,7 @@ async def copy_message(self, caption: typing.Optional[base.String] = None, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -410,6 +422,10 @@ async def copy_message(self, target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param from_chat_id: Unique identifier for the chat where the original message was sent (or channel username in the format @channelusername) :type from_chat_id: :obj:`typing.Union[base.Integer, base.String]` @@ -473,6 +489,7 @@ async def send_photo(self, caption: typing.Optional[base.String] = None, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -490,6 +507,10 @@ async def send_photo(self, :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param photo: Photo to send :type photo: :obj:`typing.Union[base.InputFile, base.String]` @@ -550,6 +571,7 @@ async def send_audio(self, performer: typing.Optional[base.String] = None, title: typing.Optional[base.String] = None, thumb: typing.Union[base.InputFile, base.String, None] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -570,6 +592,10 @@ async def send_audio(self, :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param audio: Audio file to send :type audio: :obj:`typing.Union[base.InputFile, base.String]` @@ -641,6 +667,7 @@ async def send_document(self, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, disable_content_type_detection: typing.Optional[base.Boolean] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -662,6 +689,11 @@ async def send_document(self, target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + + :param document: File to send :type document: :obj:`typing.Union[base.InputFile, base.String]` @@ -735,6 +767,7 @@ async def send_video(self, chat_id: typing.Union[base.Integer, base.String], parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, supports_streaming: typing.Optional[base.Boolean] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -753,6 +786,10 @@ async def send_video(self, chat_id: typing.Union[base.Integer, base.String], :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param video: Video to send :type video: :obj:`typing.Union[base.InputFile, base.String]` @@ -829,6 +866,7 @@ async def send_animation(self, caption: typing.Optional[base.String] = None, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -850,6 +888,10 @@ async def send_animation(self, (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param animation: Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data @@ -923,6 +965,7 @@ async def send_voice(self, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, duration: typing.Optional[base.Integer] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -944,6 +987,10 @@ async def send_voice(self, :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param voice: Audio file to send :type voice: :obj:`typing.Union[base.InputFile, base.String]` @@ -1002,6 +1049,7 @@ async def send_video_note(self, chat_id: typing.Union[base.Integer, base.String] duration: typing.Optional[base.Integer] = None, length: typing.Optional[base.Integer] = None, thumb: typing.Union[base.InputFile, base.String, None] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -1020,6 +1068,10 @@ async def send_video_note(self, chat_id: typing.Union[base.Integer, base.String] :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param video_note: Video note to send :type video_note: :obj:`typing.Union[base.InputFile, base.String]` @@ -1068,6 +1120,7 @@ async def send_video_note(self, chat_id: typing.Union[base.Integer, base.String] async def send_media_group(self, chat_id: typing.Union[base.Integer, base.String], media: typing.Union[types.MediaGroup, typing.List], + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -1085,6 +1138,10 @@ async def send_media_group(self, target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param media: A JSON-serialized array describing messages to be sent, must include 2-10 items :type media: :obj:`typing.Union[types.MediaGroup, typing.List]` @@ -1133,6 +1190,7 @@ async def send_location(self, chat_id: typing.Union[base.Integer, base.String], live_period: typing.Optional[base.Integer] = None, heading: typing.Optional[base.Integer] = None, proximity_alert_radius: typing.Optional[base.Integer] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -1150,6 +1208,10 @@ async def send_location(self, chat_id: typing.Union[base.Integer, base.String], :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param latitude: Latitude of the location :type latitude: :obj:`base.Float` @@ -1305,6 +1367,7 @@ async def send_venue(self, foursquare_type: typing.Optional[base.String] = None, google_place_id: typing.Optional[base.String] = None, google_place_type: typing.Optional[base.String] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -1323,6 +1386,10 @@ async def send_venue(self, target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param latitude: Latitude of the venue :type latitude: :obj:`base.Float` @@ -1386,6 +1453,7 @@ async def send_contact(self, chat_id: typing.Union[base.Integer, base.String], phone_number: base.String, first_name: base.String, last_name: typing.Optional[base.String] = None, vcard: typing.Optional[base.String] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -1403,6 +1471,10 @@ async def send_contact(self, chat_id: typing.Union[base.Integer, base.String], :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param phone_number: Contact's phone number :type phone_number: :obj:`base.String` @@ -1463,6 +1535,7 @@ async def send_poll(self, datetime.timedelta, None] = None, is_closed: typing.Optional[base.Boolean] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -1482,6 +1555,10 @@ async def send_poll(self, target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param question: Poll question, 1-300 characters :type question: :obj:`base.String` @@ -1569,6 +1646,7 @@ async def send_poll(self, async def send_dice(self, chat_id: typing.Union[base.Integer, base.String], + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, emoji: typing.Optional[base.String] = None, @@ -1589,6 +1667,10 @@ async def send_dice(self, target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of β€œπŸŽ²β€, β€œπŸŽ―β€, β€œπŸ€β€, β€œβš½β€, or β€œπŸŽ°β€. Dice can have values 1-6 for β€œπŸŽ²β€ and β€œπŸŽ―β€, values 1-5 for β€œπŸ€β€ and β€œβš½β€, and values 1-64 for β€œπŸŽ°β€. @@ -1930,8 +2012,8 @@ async def promote_chat_member(self, :rtype: :obj:`base.Boolean` """ if can_manage_voice_chats: - warnings.warn( - "Argument `can_manage_voice_chats` was renamed to `can_manage_video_chats` and will be removed in aiogram 2.21") + warnings.warn("Argument `can_manage_voice_chats` was renamed to `can_manage_video_chats` and will be " + "removed in aiogram 2.21") can_manage_video_chats = can_manage_voice_chats can_manage_voice_chats = None @@ -2954,6 +3036,7 @@ async def delete_message(self, chat_id: typing.Union[base.Integer, base.String], async def send_sticker(self, chat_id: typing.Union[base.Integer, base.String], sticker: typing.Union[base.InputFile, base.String], + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -2971,6 +3054,10 @@ async def send_sticker(self, chat_id: typing.Union[base.Integer, base.String], :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param sticker: Sticker to send :type sticker: :obj:`typing.Union[base.InputFile, base.String]` @@ -3338,6 +3425,7 @@ async def send_invoice(self, send_phone_number_to_provider: typing.Optional[base.Boolean] = None, send_email_to_provider: typing.Optional[base.Boolean] = None, is_flexible: typing.Optional[base.Boolean] = None, + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -3354,6 +3442,10 @@ async def send_invoice(self, @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param title: Product name, 1-32 characters :type title: :obj:`base.String` diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 32987899db..f875817e9a 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -1450,7 +1450,7 @@ class PromoteChatMember(BaseResponse): __slots__ = ('chat_id', 'user_id', 'can_change_info', 'can_post_messages', 'can_edit_messages', 'can_delete_messages', 'can_invite_users', 'can_restrict_members', 'can_pin_messages', - 'can_promote_members') + 'can_manage_topics', 'can_promote_members') method = api.Methods.PROMOTE_CHAT_MEMBER @@ -1463,6 +1463,7 @@ def __init__(self, chat_id: Union[Integer, String], can_invite_users: Optional[Boolean] = None, can_restrict_members: Optional[Boolean] = None, can_pin_messages: Optional[Boolean] = None, + can_manage_topics: Optional[Boolean] = None, can_promote_members: Optional[Boolean] = None): """ :param chat_id: Union[Integer, String] - Unique identifier for the target chat @@ -1477,6 +1478,8 @@ def __init__(self, chat_id: Union[Integer, String], :param can_invite_users: Boolean - Pass True, if the administrator can invite new users to the chat :param can_restrict_members: Boolean - Pass True, if the administrator can restrict, ban or unban chat members :param can_pin_messages: Boolean - Pass True, if the administrator can pin messages, supergroups only + :param can_manage_topics: Boolean - Pass True if the user is allowed to create, rename, close, and reopen forum + topics, supergroups only :param can_promote_members: Boolean - Pass True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) @@ -1490,6 +1493,7 @@ def __init__(self, chat_id: Union[Integer, String], self.can_invite_users = can_invite_users self.can_restrict_members = can_restrict_members self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics self.can_promote_members = can_promote_members def prepare(self): @@ -1503,6 +1507,7 @@ def prepare(self): 'can_invite_users': self.can_invite_users, 'can_restrict_members': self.can_restrict_members, 'can_pin_messages': self.can_pin_messages, + 'can_manage_topics': self.can_manage_topics, 'can_promote_members': self.can_promote_members } diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 2c272fefdb..87dc1e9ee1 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -29,6 +29,9 @@ from .encrypted_passport_element import EncryptedPassportElement from .file import File from .force_reply import ForceReply +from .forum_topic_created import ForumTopicCreated +from .forum_topic_closed import ForumTopicClosed +from .forum_topic_reopened import ForumTopicReopened from .game import Game from .game_high_score import GameHighScore from .inline_keyboard import InlineKeyboardButton, InlineKeyboardMarkup @@ -240,6 +243,9 @@ 'WebAppData', 'WebAppInfo', 'WebhookInfo', + 'ForumTopicCreated', + 'ForumTopicClosed', + 'ForumTopicReopened', 'base', 'fields', ) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 17988b7d2a..02afc9c81f 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -27,8 +27,11 @@ class Chat(base.TelegramObject): username: base.String = fields.Field() first_name: base.String = fields.Field() last_name: base.String = fields.Field() + is_forum: base.Boolean = fields.Field() all_members_are_administrators: base.Boolean = fields.Field() photo: ChatPhoto = fields.Field(base=ChatPhoto) + active_usernames: typing.List[base.String] = fields.Field() + emoji_status_custom_emoji_id: base.String = fields.Field() bio: base.String = fields.Field() has_private_forwards: base.Boolean = fields.Field() has_restricted_voice_and_video_messages: base.Boolean = fields.Field() diff --git a/aiogram/types/chat_administrator_rights.py b/aiogram/types/chat_administrator_rights.py index 4e909db8f4..d55ea0570d 100644 --- a/aiogram/types/chat_administrator_rights.py +++ b/aiogram/types/chat_administrator_rights.py @@ -19,6 +19,7 @@ class ChatAdministratorRights(base.TelegramObject): can_post_messages: base.Boolean = fields.Field() can_edit_messages: base.Boolean = fields.Field() can_pin_messages: base.Boolean = fields.Field() + can_manage_topics: base.Boolean = fields.Field() def __init__(self, is_anonymous: base.Boolean = None, @@ -31,7 +32,8 @@ def __init__(self, can_invite_users: base.Boolean = None, can_post_messages: base.Boolean = None, can_edit_messages: base.Boolean = None, - can_pin_messages: base.Boolean = None): + can_pin_messages: base.Boolean = None, + can_manage_topics: base.Boolean = None): super(ChatAdministratorRights, self).__init__( is_anonymous=is_anonymous, can_manage_chat=can_manage_chat, @@ -43,4 +45,5 @@ def __init__(self, can_invite_users=can_invite_users, can_post_messages=can_post_messages, can_edit_messages=can_edit_messages, - can_pin_messages=can_pin_messages) + can_pin_messages=can_pin_messages, + can_manage_topics=can_manage_topics) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 9a78bca290..6424025f89 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -135,6 +135,7 @@ class ChatMemberOwner(ChatMember): can_change_info: base.Boolean = fields.ConstField(True) can_invite_users: base.Boolean = fields.ConstField(True) can_pin_messages: base.Boolean = fields.ConstField(True) + can_manage_topics: base.Boolean = fields.ConstField(True) class ChatMemberAdministrator(ChatMember): @@ -159,6 +160,7 @@ class ChatMemberAdministrator(ChatMember): can_change_info: base.Boolean = fields.Field() can_invite_users: base.Boolean = fields.Field() can_pin_messages: base.Boolean = fields.Field() + can_manage_topics: base.Boolean = fields.Field() class ChatMemberMember(ChatMember): @@ -185,6 +187,7 @@ class ChatMemberRestricted(ChatMember): can_change_info: base.Boolean = fields.Field() can_invite_users: base.Boolean = fields.Field() can_pin_messages: base.Boolean = fields.Field() + can_manage_topics: base.Boolean = fields.Field() can_send_messages: base.Boolean = fields.Field() can_send_media_messages: base.Boolean = fields.Field() can_send_polls: base.Boolean = fields.Field() diff --git a/aiogram/types/chat_permissions.py b/aiogram/types/chat_permissions.py index 9d44653edb..4a40468886 100644 --- a/aiogram/types/chat_permissions.py +++ b/aiogram/types/chat_permissions.py @@ -16,6 +16,7 @@ class ChatPermissions(base.TelegramObject): can_change_info: base.Boolean = fields.Field() can_invite_users: base.Boolean = fields.Field() can_pin_messages: base.Boolean = fields.Field() + can_manage_topics: base.Boolean = fields.Field() def __init__(self, can_send_messages: base.Boolean = None, @@ -26,6 +27,7 @@ def __init__(self, can_change_info: base.Boolean = None, can_invite_users: base.Boolean = None, can_pin_messages: base.Boolean = None, + can_manage_topics: base.Boolean = None, **kwargs): super(ChatPermissions, self).__init__( can_send_messages=can_send_messages, @@ -36,4 +38,5 @@ def __init__(self, can_change_info=can_change_info, can_invite_users=can_invite_users, can_pin_messages=can_pin_messages, + can_manage_topics=can_manage_topics, ) diff --git a/aiogram/types/forum_topic_closed.py b/aiogram/types/forum_topic_closed.py new file mode 100644 index 0000000000..3bcac71218 --- /dev/null +++ b/aiogram/types/forum_topic_closed.py @@ -0,0 +1,10 @@ +from . import base + + +class ForumTopicClosed(base.TelegramObject): + """ + This object represents a service message about a forum topic closed in the chat. Currently holds no information. + + https://core.telegram.org/bots/api#forumtopicclosed + """ + pass diff --git a/aiogram/types/forum_topic_created.py b/aiogram/types/forum_topic_created.py new file mode 100644 index 0000000000..ac348206ed --- /dev/null +++ b/aiogram/types/forum_topic_created.py @@ -0,0 +1,13 @@ +from . import base +from . import fields + + +class ForumTopicCreated(base.TelegramObject): + """ + This object represents a service message about a new forum topic created in the chat. + + https://core.telegram.org/bots/api#forumtopiccreated + """ + name: base.String = fields.Field() + icon_color: base.Integer = fields.Field() + icon_custom_emoji_id: base.String = fields.Field() diff --git a/aiogram/types/forum_topic_reopened.py b/aiogram/types/forum_topic_reopened.py new file mode 100644 index 0000000000..45c575cff0 --- /dev/null +++ b/aiogram/types/forum_topic_reopened.py @@ -0,0 +1,10 @@ +from . import base + + +class ForumTopicReopened(base.TelegramObject): + """ + This object represents a service message about a forum topic closed in the chat. Currently holds no information. + + https://core.telegram.org/bots/api#forumtopicreopened + """ + pass diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 57dcff4423..4cc319fa91 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -41,6 +41,9 @@ from .voice_chat_scheduled import VoiceChatScheduled from .voice_chat_started import VoiceChatStarted from .web_app_data import WebAppData +from .forum_topic_created import ForumTopicCreated +from .forum_topic_closed import ForumTopicClosed +from .forum_topic_reopened import ForumTopicReopened from ..utils import helper from ..utils import markdown as md from ..utils.text_decorations import html_decoration, markdown_decoration @@ -54,6 +57,7 @@ class Message(base.TelegramObject): """ message_id: base.Integer = fields.Field() + message_thread_id: base.Integer = fields.Field() from_user: User = fields.Field(alias="from", base=User) sender_chat: Chat = fields.Field(base=Chat) date: datetime.datetime = fields.DateTimeField() @@ -63,6 +67,7 @@ class Message(base.TelegramObject): forward_from_message_id: base.Integer = fields.Field() forward_signature: base.String = fields.Field() forward_date: datetime.datetime = fields.DateTimeField() + is_topic_message: base.Boolean = fields.Field() is_automatic_forward: base.Boolean = fields.Field() reply_to_message: Message = fields.Field(base="Message") via_bot: User = fields.Field(base=User) @@ -112,6 +117,9 @@ class Message(base.TelegramObject): voice_chat_participants_invited: VoiceChatParticipantsInvited = fields.Field(base=VoiceChatParticipantsInvited) reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup) web_app_data: WebAppData = fields.Field(base=WebAppData) + forum_topic_created: ForumTopicCreated = fields.Field(base=ForumTopicCreated) + forum_topic_closed: ForumTopicClosed = fields.Field(base=ForumTopicClosed) + forum_topic_reopened: ForumTopicReopened = fields.Field(base=ForumTopicReopened) video_chat_scheduled: VideoChatScheduled = fields.Field(base=VideoChatScheduled) video_chat_started: VideoChatStarted = fields.Field(base=VideoChatStarted) video_chat_ended: VideoChatEnded = fields.Field(base=VideoChatEnded) @@ -278,7 +286,7 @@ def from_id(self) -> int: :return: int """ return self.sender_chat.id if self.sender_chat else self.from_user.id - + @property def md_text(self) -> str: """ @@ -396,6 +404,7 @@ async def answer( """ return await self.bot.send_message( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, text=text, parse_mode=parse_mode, entities=entities, @@ -468,6 +477,7 @@ async def answer_photo( """ return await self.bot.send_photo( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, photo=photo, caption=caption, parse_mode=parse_mode, @@ -560,6 +570,7 @@ async def answer_audio( """ return await self.bot.send_audio( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, audio=audio, caption=caption, parse_mode=parse_mode, @@ -658,6 +669,7 @@ async def answer_animation( """ return await self.bot.send_animation( self.chat.id, + message_thread_id=self.message_thread_id, animation=animation, duration=duration, width=width, @@ -749,6 +761,7 @@ async def answer_document( """ return await self.bot.send_document( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, thumb=thumb, document=document, caption=caption, @@ -807,7 +820,8 @@ async def answer_video( A thumbnailβ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` - :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after + entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -845,6 +859,7 @@ async def answer_video( """ return await self.bot.send_video( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, video=video, duration=duration, width=width, @@ -930,6 +945,7 @@ async def answer_voice( """ return await self.bot.send_voice( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, voice=voice, caption=caption, parse_mode=parse_mode, @@ -1003,6 +1019,7 @@ async def answer_video_note( """ return await self.bot.send_video_note( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, video_note=video_note, duration=duration, length=length, @@ -1053,6 +1070,7 @@ async def answer_media_group( """ return await self.bot.send_media_group( self.chat.id, + message_thread_id=self.message_thread_id, media=media, disable_notification=disable_notification, protect_content=protect_content, @@ -1131,6 +1149,7 @@ async def answer_location( """ return await self.bot.send_location( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, latitude=latitude, longitude=longitude, horizontal_accuracy=horizontal_accuracy, @@ -1223,6 +1242,7 @@ async def answer_venue( """ return await self.bot.send_venue( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, latitude=latitude, longitude=longitude, title=title, @@ -1293,6 +1313,7 @@ async def answer_contact( """ return await self.bot.send_contact( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, phone_number=phone_number, first_name=first_name, last_name=last_name, @@ -1350,6 +1371,7 @@ async def answer_sticker( """ return await self.bot.send_sticker( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, sticker=sticker, disable_notification=disable_notification, protect_content=protect_content, @@ -1463,6 +1485,7 @@ async def answer_poll( """ return await self.bot.send_poll( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, question=question, options=options, is_anonymous=is_anonymous, @@ -1536,6 +1559,7 @@ async def answer_dice( """ return await self.bot.send_dice( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, emoji=emoji, disable_notification=disable_notification, protect_content=protect_content, @@ -1627,6 +1651,7 @@ async def reply( """ return await self.bot.send_message( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, text=text, parse_mode=parse_mode, entities=entities, @@ -1699,6 +1724,7 @@ async def reply_photo( """ return await self.bot.send_photo( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, photo=photo, caption=caption, parse_mode=parse_mode, @@ -1791,6 +1817,7 @@ async def reply_audio( """ return await self.bot.send_audio( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, audio=audio, caption=caption, parse_mode=parse_mode, @@ -1889,6 +1916,7 @@ async def reply_animation( """ return await self.bot.send_animation( self.chat.id, + message_thread_id=self.message_thread_id, animation=animation, duration=duration, width=width, @@ -1980,6 +2008,7 @@ async def reply_document( """ return await self.bot.send_document( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, document=document, thumb=thumb, caption=caption, @@ -2038,7 +2067,8 @@ async def reply_video( A thumbnailβ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` - :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after + entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -2076,6 +2106,7 @@ async def reply_video( """ return await self.bot.send_video( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, video=video, duration=duration, width=width, @@ -2161,6 +2192,7 @@ async def reply_voice( """ return await self.bot.send_voice( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, voice=voice, caption=caption, parse_mode=parse_mode, @@ -2234,6 +2266,7 @@ async def reply_video_note( """ return await self.bot.send_video_note( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, video_note=video_note, duration=duration, length=length, @@ -2284,6 +2317,7 @@ async def reply_media_group( """ return await self.bot.send_media_group( self.chat.id, + message_thread_id=self.message_thread_id, media=media, disable_notification=disable_notification, protect_content=protect_content, @@ -2357,6 +2391,7 @@ async def reply_location( """ return await self.bot.send_location( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, latitude=latitude, longitude=longitude, horizontal_accuracy=horizontal_accuracy, @@ -2448,6 +2483,7 @@ async def reply_venue( """ return await self.bot.send_venue( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, latitude=latitude, longitude=longitude, title=title, @@ -2518,6 +2554,7 @@ async def reply_contact( """ return await self.bot.send_contact( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, phone_number=phone_number, first_name=first_name, last_name=last_name, @@ -2633,6 +2670,7 @@ async def reply_poll( """ return await self.bot.send_poll( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, question=question, options=options, is_anonymous=is_anonymous, @@ -2699,6 +2737,7 @@ async def reply_sticker( """ return await self.bot.send_sticker( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, sticker=sticker, disable_notification=disable_notification, protect_content=protect_content, @@ -2761,6 +2800,7 @@ async def reply_dice( """ return await self.bot.send_dice( chat_id=self.chat.id, + message_thread_id=self.message_thread_id, emoji=emoji, disable_notification=disable_notification, protect_content=protect_content, @@ -2772,6 +2812,7 @@ async def reply_dice( async def forward( self, chat_id: typing.Union[base.Integer, base.String], + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, protect_content: typing.Optional[base.Boolean] = None, ) -> Message: @@ -2783,6 +2824,10 @@ async def forward( :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_thread_id: Unique identifier for the target message thread (topic) of the forum; for forum + supergroups only + :type message_thread_id: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Optional[base.Boolean]` @@ -2795,6 +2840,7 @@ async def forward( """ return await self.bot.forward_message( chat_id=chat_id, + message_thread_id=self.message_thread_id, from_chat_id=self.chat.id, message_id=self.message_id, disable_notification=disable_notification, @@ -3059,6 +3105,7 @@ async def unpin(self) -> base.Boolean: async def send_copy( self: Message, chat_id: typing.Union[str, int], + message_thread_id: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[bool] = None, protect_content: typing.Optional[base.Boolean] = None, disable_web_page_preview: typing.Optional[bool] = None, @@ -3072,6 +3119,7 @@ async def send_copy( Send copy of current message :param chat_id: + :param message_thread_id: :param disable_notification: :param protect_content: :param disable_web_page_preview: for text messages only @@ -3082,6 +3130,7 @@ async def send_copy( """ kwargs = { "chat_id": chat_id, + "message_thread_id": message_thread_id, "allow_sending_without_reply": allow_sending_without_reply, "reply_markup": reply_markup or self.reply_markup, "parse_mode": ParseMode.HTML, diff --git a/aiogram/types/video_chat_started.py b/aiogram/types/video_chat_started.py index ec1aefd10a..ca0acb2084 100644 --- a/aiogram/types/video_chat_started.py +++ b/aiogram/types/video_chat_started.py @@ -4,7 +4,7 @@ class VideoChatStarted(base.TelegramObject, mixins.Downloadable): """ - his object represents a service message about a video chat started in the chat. Currently holds no information. + This object represents a service message about a video chat started in the chat. Currently holds no information. https://core.telegram.org/bots/api#videochatstarted """ diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 41ec0fc992..72db1420bf 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -61,6 +61,7 @@ "can_promote_members": False, "can_manage_voice_chats": True, # Deprecated "can_manage_video_chats": True, + "can_manage_topics": True, "is_anonymous": False, } diff --git a/tests/types/test_chat_member.py b/tests/types/test_chat_member.py index 2fe3e677ae..b45a4bcfb6 100644 --- a/tests/types/test_chat_member.py +++ b/tests/types/test_chat_member.py @@ -41,6 +41,9 @@ def test_privileges(): assert isinstance(chat_member.can_promote_members, bool) assert chat_member.can_promote_members == CHAT_MEMBER['can_promote_members'] + assert isinstance(chat_member.can_manage_topics, bool) + assert chat_member.can_manage_topics == CHAT_MEMBER['can_manage_topics'] + def test_int(): assert int(chat_member) == chat_member.user.id