From 74d1528e95de30d473784f5d5e9161cc3946b824 Mon Sep 17 00:00:00 2001 From: Poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Sat, 5 Nov 2022 14:00:05 +0100 Subject: [PATCH 01/29] Feat: New attributes for chat --- telegram/_chat.py | 41 +++++++++++++++++++++++++++++++++++++++++ tests/test_chat.py | 15 +++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/telegram/_chat.py b/telegram/_chat.py index 03b961624a4..6cbedb2037b 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -148,6 +148,23 @@ class Chat(TelegramObject): in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 + is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum + (has `topics `_ enabled). + + .. versionadded:: 20.0 + active_usernames (List[:obj:`str`], optional): If set, the list of all `active chat + usernames `_; for private chats, supergroups and channels. Returned + only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji + status of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + Attributes: id (:obj:`int`): Unique identifier for this chat. type (:obj:`str`): Type of chat. @@ -204,6 +221,21 @@ class Chat(TelegramObject): privacy settings of the other party restrict sending voice and video note messages in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 + is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum + (has `topics `_ enabled). + + .. versionadded:: 20.0 + active_usernames (List[:obj:`str`]): Optional. If set, the list of all `active chat + usernames `_; for private chats, supergroups and channels. Returned + only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + emoji_status_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji + status of the other party in a private chat. Returned only in + :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 """ @@ -232,6 +264,9 @@ class Chat(TelegramObject): "join_to_send_messages", "join_by_request", "has_restricted_voice_and_video_messages", + "is_forum", + "active_usernames", + "emoji_status_custom_emoji_id" ) SENDER: ClassVar[str] = constants.ChatType.SENDER @@ -273,6 +308,9 @@ def __init__( join_to_send_messages: bool = None, join_by_request: bool = None, has_restricted_voice_and_video_messages: bool = None, + is_forum: bool = None, + active_usernames: List[str] = None, + emoji_status_custom_emoji_id: str = None, *, api_kwargs: JSONDict = None, ): @@ -304,6 +342,9 @@ def __init__( self.join_to_send_messages = join_to_send_messages self.join_by_request = join_by_request self.has_restricted_voice_and_video_messages = has_restricted_voice_and_video_messages + self.is_forum = is_forum + self.active_usernames = active_usernames + self.emoji_status_custom_emoji_id = emoji_status_custom_emoji_id self._id_attrs = (self.id,) diff --git a/tests/test_chat.py b/tests/test_chat.py index ddc9d3cdbd8..8422148c0fb 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -44,6 +44,9 @@ def chat(bot): join_to_send_messages=True, join_by_request=True, has_restricted_voice_and_video_messages=True, + is_forum=True, + active_usernames=TestChat.active_usernames, + emoji_status_custom_emoji_id=TestChat.emoji_status_custom_emoji_id, ) chat.set_bot(bot) return chat @@ -71,6 +74,9 @@ class TestChat: join_to_send_messages = True join_by_request = True has_restricted_voice_and_video_messages = True + is_forum = True + active_usernames = ["These", "Are", "Usernames!"] + emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID" def test_slot_behaviour(self, chat, mro_slots): for attr in chat.__slots__: @@ -98,6 +104,9 @@ def test_de_json(self, bot): "has_restricted_voice_and_video_messages": ( self.has_restricted_voice_and_video_messages ), + "is_forum": self.is_forum, + "active_usernames": self.active_usernames, + "emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id } chat = Chat.de_json(json_dict, bot) @@ -124,6 +133,9 @@ def test_de_json(self, bot): assert chat.api_kwargs == { "all_members_are_administrators": self.all_members_are_administrators } + assert chat.is_forum == self.is_forum + assert chat.active_usernames == self.active_usernames + assert chat.emoji_status_custom_emoji_id == self.emoji_status_custom_emoji_id def test_to_dict(self, chat): chat_dict = chat.to_dict() @@ -146,6 +158,9 @@ def test_to_dict(self, chat): chat_dict["has_restricted_voice_and_video_messages"] == chat.has_restricted_voice_and_video_messages ) + assert chat_dict["is_forum"] == chat.is_forum + assert chat_dict["active_usernames"] == chat.active_usernames + assert chat_dict["emoji_status_custom_emoji_id"] == chat.emoji_status_custom_emoji_id def test_enum_init(self): chat = Chat(id=1, type="foo") From 2cd6c546a9e3f6069a3f7b49ce6c83e4705a72b6 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 6 Nov 2022 22:20:21 +0100 Subject: [PATCH 02/29] Update fallback bots and add forum_group_id fixture --- tests/bots.py | 15 ++++++++------- tests/conftest.py | 5 +++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/bots.py b/tests/bots.py index 69927a32cdc..4ed5160a5ac 100644 --- a/tests/bots.py +++ b/tests/bots.py @@ -28,13 +28,14 @@ FALLBACKS = ( "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj" - "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3J" - "hbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRi" - "X2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4MTk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1O" - "URNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsIC" - "JjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJjaGFubmVsX2l" - "kIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgImJv" - "dF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d" + "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE" + "1OTQwNCIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0" + "cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4M" + "Tk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1OURNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOi" + "AiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsICJjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm9" + "1cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE1OTQwNCIsICJjaGFubmVs" + "X2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgI" + "mJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d " ) GITHUB_ACTION = os.getenv("GITHUB_ACTION", None) diff --git a/tests/conftest.py b/tests/conftest.py index 81b611b1da6..9d56aab7619 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -207,6 +207,11 @@ def super_group_id(bot_info): return bot_info["super_group_id"] +@pytest.fixture(scope="session") +def forum_group_id(bot_info): + return int(bot_info["forum_group_id"]) + + @pytest.fixture(scope="session") def channel_id(bot_info): return bot_info["channel_id"] From f083fff850b307fde01359321ee2d5dd5f318d4d Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 7 Nov 2022 08:20:55 +0100 Subject: [PATCH 03/29] doc fix --- telegram/_chat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telegram/_chat.py b/telegram/_chat.py index 6cbedb2037b..60dc7245b6c 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -149,8 +149,7 @@ class Chat(TelegramObject): .. versionadded:: 20.0 is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum - (has `topics `_ enabled). + (has topics_ enabled). .. versionadded:: 20.0 active_usernames (List[:obj:`str`], optional): If set, the list of all `active chat @@ -223,7 +222,7 @@ class Chat(TelegramObject): .. versionadded:: 20.0 is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum - (has `topics `_ enabled). + (has topics_ enabled). .. versionadded:: 20.0 active_usernames (List[:obj:`str`]): Optional. If set, the list of all `active chat @@ -238,6 +237,7 @@ class Chat(TelegramObject): .. versionadded:: 20.0 + .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups """ __slots__ = ( @@ -266,7 +266,7 @@ class Chat(TelegramObject): "has_restricted_voice_and_video_messages", "is_forum", "active_usernames", - "emoji_status_custom_emoji_id" + "emoji_status_custom_emoji_id", ) SENDER: ClassVar[str] = constants.ChatType.SENDER From 7dc634e940514976ccc769516e036eff616e4246 Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Mon, 7 Nov 2022 22:00:39 +0100 Subject: [PATCH 04/29] Fix: black --- tests/test_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_chat.py b/tests/test_chat.py index 8422148c0fb..4f2305180b8 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -106,7 +106,7 @@ def test_de_json(self, bot): ), "is_forum": self.is_forum, "active_usernames": self.active_usernames, - "emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id + "emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id, } chat = Chat.de_json(json_dict, bot) From 3e8aa6e54c2b8d90ba9f81944fef57dbdf4068e8 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Wed, 9 Nov 2022 22:17:07 +0300 Subject: [PATCH 05/29] add `message_thread_id` param to methods, `message_thread_id` and `is_topic_message` to `Message` (#3345) --- telegram/_bot.py | 120 ++++++++++++++++++++++++++++++++++++++++ telegram/_chat.py | 42 ++++++++++++++ telegram/_message.py | 66 ++++++++++++++++++++++ telegram/_user.py | 38 +++++++++++++ telegram/ext/_extbot.py | 40 ++++++++++++++ tests/test_bot.py | 26 +++++++++ 6 files changed, 332 insertions(+) diff --git a/telegram/_bot.py b/telegram/_bot.py index f526101164c..d1c0f3710ef 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -433,6 +433,7 @@ async def _send_message( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -452,6 +453,9 @@ async def _send_message( if reply_markup is not None: data["reply_markup"] = reply_markup + if message_thread_id is not None: + data["message_thread_id"] = message_thread_id + result = await self._post( endpoint, data, @@ -669,6 +673,7 @@ async def send_message( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -709,6 +714,10 @@ async def send_message( :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to @@ -751,6 +760,7 @@ async def send_message( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -836,6 +846,7 @@ async def forward_message( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -869,6 +880,10 @@ async def forward_message( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to @@ -905,6 +920,7 @@ async def forward_message( data, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -925,6 +941,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -967,7 +984,10 @@ async def send_photo( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message @@ -1024,6 +1044,7 @@ async def send_photo( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1048,6 +1069,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1101,6 +1123,10 @@ async def send_audio( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1175,6 +1201,7 @@ async def send_audio( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1197,6 +1224,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1249,6 +1277,10 @@ async def send_document( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1317,6 +1349,7 @@ async def send_document( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1334,6 +1367,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1367,6 +1401,10 @@ async def send_sticker( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1408,6 +1446,7 @@ async def send_sticker( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1433,6 +1472,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1491,6 +1531,10 @@ async def send_video( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1566,6 +1610,7 @@ async def send_video( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1586,6 +1631,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1633,6 +1679,10 @@ async def send_video_note( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1699,6 +1749,7 @@ async def send_video_note( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1723,6 +1774,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1785,6 +1837,10 @@ async def send_animation( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1849,6 +1905,7 @@ async def send_animation( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1870,6 +1927,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1922,6 +1980,10 @@ async def send_voice( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1981,6 +2043,7 @@ async def send_voice( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1999,6 +2062,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -2032,6 +2096,10 @@ async def send_media_group( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -2105,6 +2173,9 @@ async def send_media_group( if reply_to_message_id: data["reply_to_message_id"] = reply_to_message_id + if message_thread_id: + data["message_thread_id"] = message_thread_id + result = await self._post( "sendMediaGroup", data, @@ -2132,6 +2203,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: Location = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2171,6 +2243,10 @@ async def send_location( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -2238,6 +2314,7 @@ async def send_location( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2447,6 +2524,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: Venue = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2489,6 +2567,10 @@ async def send_venue( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -2571,6 +2653,7 @@ async def send_venue( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2591,6 +2674,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: Contact = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2623,6 +2707,10 @@ async def send_contact( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -2694,6 +2782,7 @@ async def send_contact( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2711,6 +2800,7 @@ async def send_game( reply_markup: InlineKeyboardMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2733,6 +2823,10 @@ async def send_game( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -2775,6 +2869,7 @@ async def send_game( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -4845,6 +4940,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -4938,6 +5034,10 @@ async def send_invoice( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -5018,6 +5118,7 @@ async def send_invoice( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -7146,6 +7247,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -7201,6 +7303,10 @@ async def send_poll( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -7268,6 +7374,7 @@ async def send_poll( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -7349,6 +7456,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -7391,6 +7499,10 @@ async def send_dice( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to @@ -7427,6 +7539,7 @@ async def send_dice( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -7897,6 +8010,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -7933,6 +8047,10 @@ async def copy_message( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread (topic) of the forum; for forum supergroups only. + + .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -7983,6 +8101,8 @@ async def copy_message( data["reply_to_message_id"] = reply_to_message_id if reply_markup: data["reply_markup"] = reply_markup + if message_thread_id: + data["message_thread_id"] = message_thread_id result = await self._post( "copyMessage", diff --git a/telegram/_chat.py b/telegram/_chat.py index 60dc7245b6c..ea27ad74462 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -1225,6 +1225,7 @@ async def send_message( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1253,6 +1254,7 @@ async def send_message( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1269,6 +1271,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1301,6 +1304,7 @@ async def send_media_group( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, @@ -1350,6 +1354,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1380,6 +1385,7 @@ async def send_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1398,6 +1404,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: "Contact" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1433,6 +1440,7 @@ async def send_contact( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_audio( @@ -1450,6 +1458,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1484,6 +1493,7 @@ async def send_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1504,6 +1514,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1541,6 +1552,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_dice( @@ -1551,6 +1563,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1581,6 +1594,7 @@ async def send_dice( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_game( @@ -1591,6 +1605,7 @@ async def send_game( reply_markup: "InlineKeyboardMarkup" = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1621,6 +1636,7 @@ async def send_game( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_invoice( @@ -1651,6 +1667,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1711,6 +1728,7 @@ async def send_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_location( @@ -1726,6 +1744,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: "Location" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1763,6 +1782,7 @@ async def send_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_animation( @@ -1780,6 +1800,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1819,6 +1840,7 @@ async def send_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_sticker( @@ -1829,6 +1851,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1859,6 +1882,7 @@ async def send_sticker( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_venue( @@ -1876,6 +1900,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: "Venue" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1915,6 +1940,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video( @@ -1933,6 +1959,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1973,6 +2000,7 @@ async def send_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video_note( @@ -1986,6 +2014,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2021,6 +2050,7 @@ async def send_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_voice( @@ -2035,6 +2065,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2071,6 +2102,7 @@ async def send_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_poll( @@ -2092,6 +2124,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2133,6 +2166,7 @@ async def send_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_copy( @@ -2147,6 +2181,7 @@ async def send_copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2181,6 +2216,7 @@ async def send_copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def copy_message( @@ -2195,6 +2231,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2229,6 +2266,7 @@ async def copy_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def forward_from( @@ -2237,6 +2275,7 @@ async def forward_from( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2269,6 +2308,7 @@ async def forward_from( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def forward_to( @@ -2277,6 +2317,7 @@ async def forward_to( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2309,6 +2350,7 @@ async def forward_to( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def export_invite_link( diff --git a/telegram/_message.py b/telegram/_message.py index 86d40a1a355..cbbbfc3b581 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -247,6 +247,13 @@ class Message(TelegramObject): .. versionadded:: 20.0 reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. ``login_url`` buttons are represented as ordinary url buttons. + is_topic_message (:obj:`bool`, optional): True, if the message is sent to a forum topic. + + .. versionadded:: 20.0 + message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 20.0 Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -367,6 +374,13 @@ class Message(TelegramObject): .. versionadded:: 20.0 reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. + is_topic_message (:obj:`bool`, optional): True, if the message is sent to a forum topic. + + .. versionadded:: 20.0 + message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 20.0 .. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored by this function. Instead, the supplied replacement for the emoji will be used. @@ -434,6 +448,8 @@ class Message(TelegramObject): "is_automatic_forward", "has_protected_content", "web_app_data", + "is_topic_message", + "message_thread_id", ) def __init__( @@ -497,6 +513,8 @@ def __init__( is_automatic_forward: bool = None, has_protected_content: bool = None, web_app_data: WebAppData = None, + is_topic_message: bool = None, + message_thread_id: int = None, *, api_kwargs: JSONDict = None, ): @@ -563,6 +581,8 @@ def __init__( self.video_chat_participants_invited = video_chat_participants_invited self.reply_markup = reply_markup self.web_app_data = web_app_data + self.is_topic_message = is_topic_message + self.message_thread_id = message_thread_id self._effective_attachment = DEFAULT_NONE @@ -775,6 +795,7 @@ async def reply_text( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -810,6 +831,7 @@ async def reply_text( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -827,6 +849,7 @@ async def reply_markdown( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -872,6 +895,7 @@ async def reply_markdown( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -889,6 +913,7 @@ async def reply_markdown_v2( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -930,6 +955,7 @@ async def reply_markdown_v2( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -947,6 +973,7 @@ async def reply_html( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -988,6 +1015,7 @@ async def reply_html( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1004,6 +1032,7 @@ async def reply_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1046,6 +1075,7 @@ async def reply_media_group( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, @@ -1062,6 +1092,7 @@ async def reply_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1099,6 +1130,7 @@ async def reply_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1121,6 +1153,7 @@ async def reply_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1162,6 +1195,7 @@ async def reply_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1182,6 +1216,7 @@ async def reply_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1226,6 +1261,7 @@ async def reply_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_animation( @@ -1243,6 +1279,7 @@ async def reply_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1290,6 +1327,7 @@ async def reply_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_sticker( @@ -1300,6 +1338,7 @@ async def reply_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1337,6 +1376,7 @@ async def reply_sticker( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_video( @@ -1355,6 +1395,7 @@ async def reply_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1402,6 +1443,7 @@ async def reply_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_video_note( @@ -1415,6 +1457,7 @@ async def reply_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1458,6 +1501,7 @@ async def reply_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_voice( @@ -1472,6 +1516,7 @@ async def reply_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, quote: bool = None, @@ -1516,6 +1561,7 @@ async def reply_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_location( @@ -1531,6 +1577,7 @@ async def reply_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: Location = None, quote: bool = None, @@ -1575,6 +1622,7 @@ async def reply_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_venue( @@ -1592,6 +1640,7 @@ async def reply_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: Venue = None, quote: bool = None, @@ -1638,6 +1687,7 @@ async def reply_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_contact( @@ -1651,6 +1701,7 @@ async def reply_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: Contact = None, quote: bool = None, @@ -1693,6 +1744,7 @@ async def reply_contact( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_poll( @@ -1714,6 +1766,7 @@ async def reply_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1762,6 +1815,7 @@ async def reply_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_dice( @@ -1772,6 +1826,7 @@ async def reply_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1809,6 +1864,7 @@ async def reply_dice( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_chat_action( @@ -1851,6 +1907,7 @@ async def reply_game( reply_markup: "InlineKeyboardMarkup" = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1890,6 +1947,7 @@ async def reply_game( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_invoice( @@ -1920,6 +1978,7 @@ async def reply_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1989,6 +2048,7 @@ async def reply_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def forward( @@ -1996,6 +2056,7 @@ async def forward( chat_id: Union[int, str], disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2032,6 +2093,7 @@ async def forward( message_id=self.message_id, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2050,6 +2112,7 @@ async def copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2090,6 +2153,7 @@ async def copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def reply_copy( @@ -2104,6 +2168,7 @@ async def reply_copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, quote: bool = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2152,6 +2217,7 @@ async def reply_copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def edit_text( diff --git a/telegram/_user.py b/telegram/_user.py index 01bf0d93a87..015ec93234e 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -376,6 +376,7 @@ async def send_message( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -404,6 +405,7 @@ async def send_message( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -422,6 +424,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -452,6 +455,7 @@ async def send_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -468,6 +472,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -500,6 +505,7 @@ async def send_media_group( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, @@ -520,6 +526,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -554,6 +561,7 @@ async def send_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -605,6 +613,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: "Contact" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -640,6 +649,7 @@ async def send_contact( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_dice( @@ -650,6 +660,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -680,6 +691,7 @@ async def send_dice( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_document( @@ -695,6 +707,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -732,6 +745,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_game( @@ -742,6 +756,7 @@ async def send_game( reply_markup: "InlineKeyboardMarkup" = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -772,6 +787,7 @@ async def send_game( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_invoice( @@ -802,6 +818,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -862,6 +879,7 @@ async def send_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_location( @@ -877,6 +895,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: "Location" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -914,6 +933,7 @@ async def send_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_animation( @@ -931,6 +951,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -970,6 +991,7 @@ async def send_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_sticker( @@ -980,6 +1002,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -1010,6 +1033,7 @@ async def send_sticker( api_kwargs=api_kwargs, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video( @@ -1028,6 +1052,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1068,6 +1093,7 @@ async def send_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_venue( @@ -1085,6 +1111,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: "Venue" = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1124,6 +1151,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_video_note( @@ -1137,6 +1165,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1172,6 +1201,7 @@ async def send_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_voice( @@ -1186,6 +1216,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1222,6 +1253,7 @@ async def send_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_poll( @@ -1243,6 +1275,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1284,6 +1317,7 @@ async def send_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def send_copy( @@ -1298,6 +1332,7 @@ async def send_copy( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1332,6 +1367,7 @@ async def send_copy( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def copy_message( @@ -1346,6 +1382,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1380,6 +1417,7 @@ async def copy_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) async def approve_join_request( diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index a344123f965..6a68f5a9971 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -460,6 +460,7 @@ async def _send_message( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -477,6 +478,7 @@ async def _send_message( reply_markup=self._replace_keyboard(reply_markup), allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -624,6 +626,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -645,6 +648,7 @@ async def copy_message( allow_sending_without_reply=allow_sending_without_reply, reply_markup=self._replace_keyboard(reply_markup), protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1395,6 +1399,7 @@ async def forward_message( message_id: int, disable_notification: DVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1409,6 +1414,7 @@ async def forward_message( message_id=message_id, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -1876,6 +1882,7 @@ async def send_animation( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1900,6 +1907,7 @@ async def send_animation( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1924,6 +1932,7 @@ async def send_audio( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1948,6 +1957,7 @@ async def send_audio( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1990,6 +2000,7 @@ async def send_contact( vcard: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, contact: Contact = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2010,6 +2021,7 @@ async def send_contact( vcard=vcard, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, contact=contact, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2027,6 +2039,7 @@ async def send_dice( emoji: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2043,6 +2056,7 @@ async def send_dice( emoji=emoji, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2064,6 +2078,7 @@ async def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2086,6 +2101,7 @@ async def send_document( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2103,6 +2119,7 @@ async def send_game( reply_markup: InlineKeyboardMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2119,6 +2136,7 @@ async def send_game( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2155,6 +2173,7 @@ async def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2191,6 +2210,7 @@ async def send_invoice( max_tip_amount=max_tip_amount, suggested_tip_amounts=suggested_tip_amounts, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2212,6 +2232,7 @@ async def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, location: Location = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2234,6 +2255,7 @@ async def send_location( proximity_alert_radius=proximity_alert_radius, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, location=location, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2252,6 +2274,7 @@ async def send_media_group( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -2270,6 +2293,7 @@ async def send_media_group( reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2292,6 +2316,7 @@ async def send_message( reply_to_message_id: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2308,6 +2333,7 @@ async def send_message( disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, protect_content=protect_content, + message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, @@ -2330,6 +2356,7 @@ async def send_photo( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2350,6 +2377,7 @@ async def send_photo( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2378,6 +2406,7 @@ async def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2405,6 +2434,7 @@ async def send_poll( allow_sending_without_reply=allow_sending_without_reply, explanation_entities=explanation_entities, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2421,6 +2451,7 @@ async def send_sticker( reply_markup: ReplyMarkup = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -2437,6 +2468,7 @@ async def send_sticker( reply_markup=reply_markup, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2460,6 +2492,7 @@ async def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, venue: Venue = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2484,6 +2517,7 @@ async def send_venue( google_place_type=google_place_type, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, venue=venue, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2509,6 +2543,7 @@ async def send_video( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2534,6 +2569,7 @@ async def send_video( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2554,6 +2590,7 @@ async def send_video_note( thumb: FileInput = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2574,6 +2611,7 @@ async def send_video_note( thumb=thumb, allow_sending_without_reply=allow_sending_without_reply, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2595,6 +2633,7 @@ async def send_voice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, filename: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2616,6 +2655,7 @@ async def send_voice( allow_sending_without_reply=allow_sending_without_reply, caption_entities=caption_entities, protect_content=protect_content, + message_thread_id=message_thread_id, filename=filename, read_timeout=read_timeout, write_timeout=write_timeout, diff --git a/tests/test_bot.py b/tests/test_bot.py index 0189763799a..e6655fdefe4 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2494,6 +2494,32 @@ async def test_send_message_entities(self, bot, chat_id): assert message.text == test_string assert message.entities == entities + @flaky(3, 1) + async def test_send_message_to_topic(self, bot, forum_group_id): + # TODO rework when new methods and attributes become available + test_string = "Topics are forever" + + result = await bot._post( + "createForumTopic", + {"chat_id": forum_group_id, "name": "Is just a yellow lemon tree"}, + ) + + message_thread_id = result["message_thread_id"] + + message = await bot.send_message( + chat_id=forum_group_id, text=test_string, message_thread_id=message_thread_id + ) + + assert message.text == test_string + assert message.is_topic_message is True + assert message.message_thread_id == message_thread_id + + result = await bot._post( + "deleteForumTopic", + {"chat_id": forum_group_id, "message_thread_id": message_thread_id}, + ) + assert result is True + @flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_message_default_parse_mode(self, default_bot, chat_id): From d48499fb1550d26964ad5a6bda6b70386e0832ab Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Wed, 9 Nov 2022 22:36:08 +0100 Subject: [PATCH 06/29] Feat: Docstring changes + moving limit to constants (#3343) --- telegram/_bot.py | 5 ++++- telegram/constants.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index d1c0f3710ef..3fff52d7921 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -785,6 +785,7 @@ async def delete_message( limitations: - A message can only be deleted if it was sent less than 48 hours ago. + - Service messages about a supergroup, channel, or forum topic creation can't be deleted. - A dice message in a private chat can only be deleted if it was sent more than 24 hours ago. - Bots can delete outgoing messages in private chats, groups, and supergroups. @@ -6272,7 +6273,9 @@ async def set_chat_title( Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). - title (:obj:`str`): New chat title, 1-255 characters. + title (:obj:`str`): New chat title, + tg-const:`telegram.constants.ChatLimits.MIN_TITLE_LENGTH`- + tg-const:`telegram.constants.ChatLimits.MAX_TITLE_LENGTH` characters. Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to diff --git a/telegram/constants.py b/telegram/constants.py index f5cd7b838f2..b20f847dc52 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -39,6 +39,7 @@ "ChatAction", "ChatID", "ChatInviteLinkLimit", + "ChatLimit", "ChatMemberStatus", "ChatType", "CustomEmojiStickerLimit", @@ -901,3 +902,18 @@ class WebhookLimit(IntEnum): """:obj:`int`: Minimum length of the secret token.""" MAX_SECRET_TOKEN_LENGTH = 256 """:obj:`int`: Maximum length of the secret token.""" + + +class ChatLimit(IntEnum): + """This enum contains limitations for :paramref:`telegram.Bot.set_chat_title.title`. The + enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_TITLE_LENGTH = 1 + """:obj:`int`: Minimum length of a new chat title.""" + MAX_TITLE_LENGTH = 128 + """:obj:`int`: Maximum length of a new chat title.""" From 1ebb561f2e2fba6f282da576124fbfa65038a48d Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Fri, 11 Nov 2022 13:39:32 +0100 Subject: [PATCH 07/29] Fix: Pycharm messing with me --- tests/test_bot.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 51945506dea..806dbcacde2 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2493,6 +2493,32 @@ async def test_send_message_entities(self, bot, chat_id): assert message.text == test_string assert message.entities == entities + @pytest.mark.flaky(3, 1) + async def test_send_message_to_topic(self, bot, forum_group_id): + # TODO rework when new methods and attributes become available + test_string = "Topics are forever" + + result = await bot._post( + "createForumTopic", + {"chat_id": forum_group_id, "name": "Is just a yellow lemon tree"}, + ) + + message_thread_id = result["message_thread_id"] + + message = await bot.send_message( + chat_id=forum_group_id, text=test_string, message_thread_id=message_thread_id + ) + + assert message.text == test_string + assert message.is_topic_message is True + assert message.message_thread_id == message_thread_id + + result = await bot._post( + "deleteForumTopic", + {"chat_id": forum_group_id, "message_thread_id": message_thread_id}, + ) + assert result is True + @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_message_default_parse_mode(self, default_bot, chat_id): From 7cb99c696a3d6acd715d1f3b1e0b4d6e962f9797 Mon Sep 17 00:00:00 2001 From: Clot <69784758+clot27@users.noreply.github.com> Date: Fri, 11 Nov 2022 21:03:19 +0530 Subject: [PATCH 08/29] Some part of the new api update (#3342) --- telegram/_bot.py | 7 +++++++ telegram/_chatadministratorrights.py | 25 ++++++++++++++++++++++--- telegram/_chatmember.py | 22 ++++++++++++++++++++++ telegram/_chatpermissions.py | 25 +++++++++++++++++++++---- telegram/ext/_extbot.py | 2 ++ tests/test_bot.py | 4 ++++ tests/test_chatadministratorrights.py | 17 ++++++++++------- tests/test_chatmember.py | 3 +++ tests/test_chatpermissions.py | 5 +++++ 9 files changed, 96 insertions(+), 14 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 3fff52d7921..0c4773e4ea3 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -5434,6 +5434,7 @@ async def promote_chat_member( is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -5488,6 +5489,10 @@ async def promote_chat_member( 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). + can_manage_topics (:obj: `bool`, optional): Pass :obj:`True`, if the administrator is + allowed to create, rename, close, and reopen forum topics, supergroups only. + + .. versionadded:: 20.0 Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to @@ -5536,6 +5541,8 @@ async def promote_chat_member( data["can_manage_chat"] = can_manage_chat if can_manage_video_chats is not None: data["can_manage_video_chats"] = can_manage_video_chats + if can_manage_topics is not None: + data["can_manage_topics"] = can_manage_topics result = await self._post( "promoteChatMember", diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py index 185782373ca..aaf0fd67036 100644 --- a/telegram/_chatadministratorrights.py +++ b/telegram/_chatadministratorrights.py @@ -29,7 +29,12 @@ class ChatAdministratorRights(TelegramObject): considered equal, if their :attr:`is_anonymous`, :attr:`can_manage_chat`, :attr:`can_delete_messages`, :attr:`can_manage_video_chats`, :attr:`can_restrict_members`, :attr:`can_promote_members`, :attr:`can_change_info`, :attr:`can_invite_users`, - :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages` are equal. + :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages`, + :attr:`can_manage_topics` are equal. + + .. versionchanged:: 20.0 + :attr:`can_manage_topics` is considered as well when comparing objects of + this type in terms of equality. .. seealso: :meth:`Bot.set_my_default_administrator_rights`, :meth:`Bot.get_my_default_administrator_rights` @@ -62,6 +67,10 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only. + + .. versionadded:: 20.0 Attributes: is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. @@ -89,6 +98,10 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only. + + .. versionadded:: 20.0 """ __slots__ = ( @@ -103,6 +116,7 @@ class ChatAdministratorRights(TelegramObject): "can_post_messages", "can_edit_messages", "can_pin_messages", + "can_manage_topics", ) def __init__( @@ -118,6 +132,7 @@ def __init__( can_post_messages: bool = None, can_edit_messages: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, *, api_kwargs: JSONDict = None, ) -> None: @@ -135,6 +150,7 @@ def __init__( self.can_post_messages = can_post_messages self.can_edit_messages = can_edit_messages self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics self._id_attrs = ( self.is_anonymous, @@ -148,6 +164,7 @@ def __init__( self.can_post_messages, self.can_edit_messages, self.can_pin_messages, + self.can_manage_topics, ) @classmethod @@ -159,7 +176,7 @@ def all_rights(cls) -> "ChatAdministratorRights": .. versionadded:: 20.0 """ - return cls(True, True, True, True, True, True, True, True, True, True, True) + return cls(True, True, True, True, True, True, True, True, True, True, True, True) @classmethod def no_rights(cls) -> "ChatAdministratorRights": @@ -169,4 +186,6 @@ def no_rights(cls) -> "ChatAdministratorRights": .. versionadded:: 20.0 """ - return cls(False, False, False, False, False, False, False, False, False, False, False) + return cls( + False, False, False, False, False, False, False, False, False, False, False, False + ) diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 01b40f7e195..52dcf83b697 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -215,6 +215,10 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only. + + .. versionadded:: 20.0 custom_title (:obj:`str`, optional): Custom title for this user. Attributes: @@ -252,6 +256,10 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. + can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only + + .. versionadded:: 20.0 custom_title (:obj:`str`): Optional. Custom title for this user. """ @@ -268,6 +276,7 @@ class ChatMemberAdministrator(ChatMember): "can_post_messages", "can_edit_messages", "can_pin_messages", + "can_manage_topics", "custom_title", ) @@ -286,6 +295,7 @@ def __init__( can_post_messages: bool = None, can_edit_messages: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, custom_title: str = None, *, api_kwargs: JSONDict = None, @@ -303,6 +313,7 @@ def __init__( self.can_post_messages = can_post_messages self.can_edit_messages = can_edit_messages self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics self.custom_title = custom_title @@ -361,6 +372,10 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. + can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only + + .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. @@ -386,6 +401,10 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. + can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only + + .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions will be lifted for this user. @@ -401,6 +420,7 @@ class ChatMemberRestricted(ChatMember): "can_send_polls", "can_send_other_messages", "can_add_web_page_previews", + "can_manage_topics", "until_date", ) @@ -416,6 +436,7 @@ def __init__( can_send_polls: bool, can_send_other_messages: bool, can_add_web_page_previews: bool, + can_manage_topics: bool, until_date: datetime.datetime, *, api_kwargs: JSONDict = None, @@ -430,6 +451,7 @@ def __init__( self.can_send_polls = can_send_polls self.can_send_other_messages = can_send_other_messages self.can_add_web_page_previews = can_add_web_page_previews + self.can_manage_topics = can_manage_topics self.until_date = until_date diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py index 416e3306a27..1b724a7dbb5 100644 --- a/telegram/_chatpermissions.py +++ b/telegram/_chatpermissions.py @@ -28,12 +28,17 @@ class ChatPermissions(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`, :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`, - :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages` are equal. + :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages`, + :attr:`can_manage_topics` are equal. + + .. versionchanged:: 20.0 + :attr:`can_manage_topics` is considered as well when comparing objects of + this type in terms of equality. Note: Though not stated explicitly in the official docs, Telegram changes not only the permissions that are set, but also sets all the others to :obj:`False`. However, since not - documented, this behaviour may change unbeknown to PTB. + documented, this behavior may change unbeknown to PTB. Args: can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text @@ -54,6 +59,10 @@ class ChatPermissions(TelegramObject): users to the chat. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages. Ignored in public supergroups. + can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only + + .. versionadded:: 20.0 Attributes: can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text @@ -74,6 +83,10 @@ class ChatPermissions(TelegramObject): new users to the chat. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages. Ignored in public supergroups. + can_manage_topics (:obj: `bool`): Optional. :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; supergroups only + + .. versionadded:: 20.0 """ @@ -86,6 +99,7 @@ class ChatPermissions(TelegramObject): "can_change_info", "can_pin_messages", "can_add_web_page_previews", + "can_manage_topics", ) def __init__( @@ -98,6 +112,7 @@ def __init__( can_change_info: bool = None, can_invite_users: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, *, api_kwargs: JSONDict = None, ): @@ -111,6 +126,7 @@ def __init__( self.can_change_info = can_change_info self.can_invite_users = can_invite_users self.can_pin_messages = can_pin_messages + self.can_manage_topics = can_manage_topics self._id_attrs = ( self.can_send_messages, @@ -121,6 +137,7 @@ def __init__( self.can_change_info, self.can_invite_users, self.can_pin_messages, + self.can_manage_topics, ) @classmethod @@ -133,7 +150,7 @@ def all_permissions(cls) -> "ChatPermissions": .. versionadded:: 20.0 """ - return cls(True, True, True, True, True, True, True, True) + return cls(True, True, True, True, True, True, True, True, True) @classmethod def no_permissions(cls) -> "ChatPermissions": @@ -143,4 +160,4 @@ def no_permissions(cls) -> "ChatPermissions": .. versionadded:: 20.0 """ - return cls(False, False, False, False, False, False, False, False) + return cls(False, False, False, False, False, False, False, False, False) diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 6a68f5a9971..c634042d3ff 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -1789,6 +1789,7 @@ async def promote_chat_member( is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1811,6 +1812,7 @@ async def promote_chat_member( is_anonymous=is_anonymous, can_manage_chat=can_manage_chat, can_manage_video_chats=can_manage_video_chats, + can_manage_topics=can_manage_topics, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, diff --git a/tests/test_bot.py b/tests/test_bot.py index 806dbcacde2..ce2e6963b97 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2128,6 +2128,7 @@ async def test_promote_chat_member(self, bot, channel_id, monkeypatch): can_promote_members=True, can_manage_chat=True, can_manage_video_chats=True, + can_manage_topics=True, ) # Test that we pass the correct params to TG @@ -2147,6 +2148,7 @@ async def make_assertion(*args, **_): and data.get("can_promote_members") == 9 and data.get("can_manage_chat") == 10 and data.get("can_manage_video_chats") == 11 + and data.get("can_manage_topics") == 12 ) monkeypatch.setattr(bot, "_post", make_assertion) @@ -2164,6 +2166,7 @@ async def make_assertion(*args, **_): can_promote_members=9, can_manage_chat=10, can_manage_video_chats=11, + can_manage_topics=12, ) @pytest.mark.flaky(3, 1) @@ -2604,6 +2607,7 @@ async def test_get_set_my_default_administrator_rights(self, bot): assert my_admin_rights_ch.can_promote_members is my_rights.can_promote_members assert my_admin_rights_ch.can_restrict_members is my_rights.can_restrict_members assert my_admin_rights_ch.can_pin_messages is None # Not returned for channels + assert my_admin_rights_ch.can_manage_topics is None # Not returned for channels @pytest.mark.asyncio async def test_get_set_chat_menu_button(self, bot, chat_id): diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py index 16175f252a6..83826f74bac 100644 --- a/tests/test_chatadministratorrights.py +++ b/tests/test_chatadministratorrights.py @@ -34,6 +34,7 @@ def chat_admin_rights(): can_edit_messages=True, can_manage_chat=True, can_manage_video_chats=True, + can_manage_topics=True, is_anonymous=True, ) @@ -57,6 +58,7 @@ def test_de_json(self, bot, chat_admin_rights): "can_edit_messages": True, "can_manage_chat": True, "can_manage_video_chats": True, + "can_manage_topics": True, "is_anonymous": True, } chat_administrator_rights_de = ChatAdministratorRights.de_json(json_dict, bot) @@ -80,13 +82,14 @@ def test_to_dict(self, chat_admin_rights): assert admin_rights_dict["can_manage_chat"] == car.can_manage_chat assert admin_rights_dict["is_anonymous"] == car.is_anonymous assert admin_rights_dict["can_manage_video_chats"] == car.can_manage_video_chats + assert admin_rights_dict["can_manage_topics"] == car.can_manage_topics def test_equality(self): - a = ChatAdministratorRights(True, False, False, False, False, False, False, False) - b = ChatAdministratorRights(True, False, False, False, False, False, False, False) - c = ChatAdministratorRights(False, False, False, False, False, False, False, False) - d = ChatAdministratorRights(True, True, False, False, False, False, False, False) - e = ChatAdministratorRights(True, True, False, False, False, False, False, False) + a = ChatAdministratorRights(True, False, False, False, False, False, False, False, False) + b = ChatAdministratorRights(True, False, False, False, False, False, False, False, False) + c = ChatAdministratorRights(False, False, False, False, False, False, False, False, False) + d = ChatAdministratorRights(True, True, False, False, False, False, False, False, False) + e = ChatAdministratorRights(True, True, False, False, False, False, False, False, False) assert a == b assert hash(a) == hash(b) @@ -102,7 +105,7 @@ def test_equality(self): assert hash(d) == hash(e) def test_all_rights(self): - f = ChatAdministratorRights(True, True, True, True, True, True, True, True) + f = ChatAdministratorRights(True, True, True, True, True, True, True, True, True) t = ChatAdministratorRights.all_rights() # if the dirs are the same, the attributes will all be there assert dir(f) == dir(t) @@ -114,7 +117,7 @@ def test_all_rights(self): assert f != t def test_no_rights(self): - f = ChatAdministratorRights(False, False, False, False, False, False, False, False) + f = ChatAdministratorRights(False, False, False, False, False, False, False, False, False) t = ChatAdministratorRights.no_rights() # if the dirs are the same, the attributes will all be there assert dir(f) == dir(t) diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 2e6248087d6..1c7329ef255 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -60,6 +60,7 @@ class CMDefaults: is_member: bool = True can_manage_chat: bool = True can_manage_video_chats: bool = True + can_manage_topics: bool = True def chat_member_owner(): @@ -81,6 +82,7 @@ def chat_member_administrator(): CMDefaults.can_post_messages, CMDefaults.can_edit_messages, CMDefaults.can_pin_messages, + CMDefaults.can_manage_topics, CMDefaults.custom_title, ) @@ -96,6 +98,7 @@ def chat_member_restricted(): CMDefaults.can_change_info, CMDefaults.can_invite_users, CMDefaults.can_pin_messages, + CMDefaults.can_manage_topics, CMDefaults.can_send_messages, CMDefaults.can_send_media_messages, CMDefaults.can_send_polls, diff --git a/tests/test_chatpermissions.py b/tests/test_chatpermissions.py index 64f4da6c269..5498bae24ca 100644 --- a/tests/test_chatpermissions.py +++ b/tests/test_chatpermissions.py @@ -33,6 +33,7 @@ def chat_permissions(): can_change_info=True, can_invite_users=True, can_pin_messages=True, + can_manage_topics=True, ) @@ -45,6 +46,7 @@ class TestChatPermissions: can_change_info = False can_invite_users = None can_pin_messages = None + can_manage_topics = None def test_slot_behaviour(self, chat_permissions, mro_slots): inst = chat_permissions @@ -62,6 +64,7 @@ def test_de_json(self, bot): "can_change_info": self.can_change_info, "can_invite_users": self.can_invite_users, "can_pin_messages": self.can_pin_messages, + "can_manage_topics": self.can_manage_topics, } permissions = ChatPermissions.de_json(json_dict, bot) assert permissions.api_kwargs == {} @@ -74,6 +77,7 @@ def test_de_json(self, bot): assert permissions.can_change_info == self.can_change_info assert permissions.can_invite_users == self.can_invite_users assert permissions.can_pin_messages == self.can_pin_messages + assert permissions.can_manage_topics == self.can_manage_topics def test_to_dict(self, chat_permissions): permissions_dict = chat_permissions.to_dict() @@ -94,6 +98,7 @@ def test_to_dict(self, chat_permissions): assert permissions_dict["can_change_info"] == chat_permissions.can_change_info assert permissions_dict["can_invite_users"] == chat_permissions.can_invite_users assert permissions_dict["can_pin_messages"] == chat_permissions.can_pin_messages + assert permissions_dict["can_manage_topics"] == chat_permissions.can_manage_topics def test_equality(self): a = ChatPermissions( From 48c04d5a4c8abb0cb17f9f34335715017c65ab3b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 11 Nov 2022 17:58:11 +0100 Subject: [PATCH 09/29] post-merge review of #3342 --- telegram/_bot.py | 4 ++-- telegram/_chatadministratorrights.py | 6 +++--- telegram/_chatmember.py | 13 +++++++++---- telegram/_chatpermissions.py | 10 ++++++---- tests/test_chatmember.py | 2 +- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 0c4773e4ea3..791eb14be45 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -5489,8 +5489,8 @@ async def promote_chat_member( 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). - can_manage_topics (:obj: `bool`, optional): Pass :obj:`True`, if the administrator is - allowed to create, rename, close, and reopen forum topics, supergroups only. + can_manage_topics (:obj:`bool`, optional): Pass :obj:`True`, if the administrator is + allowed to create, rename, close, and reopen forum topics; supergroups only. .. versionadded:: 20.0 diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py index aaf0fd67036..877bd279f66 100644 --- a/telegram/_chatadministratorrights.py +++ b/telegram/_chatadministratorrights.py @@ -67,7 +67,7 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. .. versionadded:: 20.0 @@ -98,10 +98,10 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`) Optional. :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. - .. versionadded:: 20.0 + .. versionadded:: 20.0 """ __slots__ = ( diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 52dcf83b697..da8cea34690 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -181,6 +181,8 @@ class ChatMemberAdministrator(ChatMember): * Argument and attribute ``can_manage_voice_chats`` were renamed to :paramref:`can_manage_video_chats` and :attr:`can_manage_video_chats` in accordance to Bot API 6.0. + * The argument :paramref:`can_manage_topics` was added, which changes the position of the + optional argument :paramref:`custom_title`. Args: user (:class:`telegram.User`): Information about the user. @@ -215,7 +217,7 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. .. versionadded:: 20.0 @@ -256,7 +258,7 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`) Optional. :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only .. versionadded:: 20.0 @@ -351,6 +353,9 @@ class ChatMemberRestricted(ChatMember): in the chat. Supergroups only. .. versionadded:: 13.7 + .. versionchanged:: 20.0 + The argument :paramref:`can_manage_topics` was added, which changes the position of the + optional argument :paramref:`until_date`. Args: user (:class:`telegram.User`): Information about the user. @@ -372,7 +377,7 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. - can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only .. versionadded:: 20.0 @@ -401,7 +406,7 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. - can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`) Optional. :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only .. versionadded:: 20.0 diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py index 1b724a7dbb5..03c8bde8bde 100644 --- a/telegram/_chatpermissions.py +++ b/telegram/_chatpermissions.py @@ -59,8 +59,9 @@ class ChatPermissions(TelegramObject): users to the chat. can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin messages. Ignored in public supergroups. - can_manage_topics (:obj: `bool`, optional): :obj:`True`, if the user is allowed - to create, rename, close, and reopen forum topics; supergroups only + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to create forum topics. If omitted defaults to the value of + :attr:`can_pin_messages`. .. versionadded:: 20.0 @@ -83,8 +84,9 @@ class ChatPermissions(TelegramObject): new users to the chat. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages. Ignored in public supergroups. - can_manage_topics (:obj: `bool`): Optional. :obj:`True`, if the user is allowed - to create, rename, close, and reopen forum topics; supergroups only + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to create forum topics. If omitted defaults to the value of + :attr:`can_pin_messages`. .. versionadded:: 20.0 diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 1c7329ef255..552755d0ad4 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -98,12 +98,12 @@ def chat_member_restricted(): CMDefaults.can_change_info, CMDefaults.can_invite_users, CMDefaults.can_pin_messages, - CMDefaults.can_manage_topics, CMDefaults.can_send_messages, CMDefaults.can_send_media_messages, CMDefaults.can_send_polls, CMDefaults.can_send_other_messages, CMDefaults.can_add_web_page_previews, + CMDefaults.can_manage_topics, CMDefaults.until_date, ) From 2fd5538737fecc4979ee2e05ee4f3bc0d45290c7 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 13 Nov 2022 21:35:02 +0100 Subject: [PATCH 10/29] temporarily enable tests for PRs agains this branch --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b62fa9d4880..ef168644966 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ on: pull_request: branches: - master + - api_6.3_update push: branches: - master From 2c8a1c0d3de3c79db4c3705b35f281c5b6104cb4 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 14 Nov 2022 20:53:29 +0100 Subject: [PATCH 11/29] update two shorcut methods --- telegram/_callbackquery.py | 2 ++ telegram/_chat.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py index 73674e0d491..2a6fd1ba682 100644 --- a/telegram/_callbackquery.py +++ b/telegram/_callbackquery.py @@ -724,6 +724,7 @@ async def copy_message( allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE, reply_markup: ReplyMarkup = None, protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: int = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -761,6 +762,7 @@ async def copy_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) MAX_ANSWER_TEXT_LENGTH: ClassVar[ diff --git a/telegram/_chat.py b/telegram/_chat.py index ea27ad74462..14946b03e53 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -835,6 +835,7 @@ async def promote_member( is_anonymous: bool = None, can_manage_chat: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -878,6 +879,7 @@ async def promote_member( is_anonymous=is_anonymous, can_manage_chat=can_manage_chat, can_manage_video_chats=can_manage_video_chats, + can_manage_topics=can_manage_topics, ) async def restrict_member( From addaa6d1545e9e83440acf42ced06d97f74c7032 Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Tue, 15 Nov 2022 18:02:51 +0100 Subject: [PATCH 12/29] 6.3 new methods (#3360) Co-authored-by: Aditya Yadav Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> --- docs/source/inclusions/bot_methods.rst | 29 ++ docs/substitutions/global.rst | 2 + telegram/_bot.py | 359 +++++++++++++++++++++++++ telegram/_chat.py | 201 ++++++++++++++ telegram/_message.py | 174 ++++++++++++ telegram/constants.py | 77 ++++++ telegram/ext/_extbot.py | 165 ++++++++++++ tests/test_bot.py | 3 + tests/test_chat.py | 122 +++++++++ tests/test_message.py | 120 +++++++++ 10 files changed, 1252 insertions(+) diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index df699e796aa..aff45cc164f 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -256,6 +256,35 @@
+.. raw:: html + +
+ Forum topic management + +.. list-table:: + :align: left + :widths: 1 4 + + * - :meth:`~telegram.Bot.close_forum_topic` + - Used for closing a forum topic + * - :meth:`~telegram.Bot.create_forum_topic` + - Used to create a topic + * - :meth:`~telegram.Bot.delete_forum_topic` + - Used for deleting a forum topic + * - :meth:`~telegram.Bot.edit_forum_topic` + - Used to edit a topic + * - :meth:`~telegram.Bot.reopen_forum_topic` + - Used to reopen a topic + * - :meth:`~telegram.Bot.get_forum_topic_icon_stickers` + - Used to get custom emojis to use as topic icons + * - :meth:`~telegram.Bot.unpin_all_forum_topic_messages` + - Used to unpin all messages in a forum topic + +.. raw:: html + +
+
+ .. raw:: html
diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index 46067cbc8a9..b06bb09db41 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -26,6 +26,8 @@ .. |chat_id_group| replace:: Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``). +.. |message_thread_id| replace:: Unique identifier for the target message thread of the forum topic. + .. |parse_mode| replace:: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. See the constants in :class:`telegram.constants.ParseMode` for the available modes. .. |allow_sending_without_reply| replace:: Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. diff --git a/telegram/_bot.py b/telegram/_bot.py index 72f1cc49ccd..d8825ef4881 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -6878,6 +6878,351 @@ async def create_invoice_link( api_kwargs=api_kwargs, ) + @_log + async def get_forum_topic_icon_stickers( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> List[Sticker]: + """Use this method to get custom emoji stickers, which can be used as a forum topic + icon by any user. Requires no parameters. + + .. versionadded:: 20.0 + + Returns: + List[:class:`telegram.Sticker`] + + Raises: + :class:`telegram.error.TelegramError` + + """ + result = await self._post( + "getForumTopicIconStickers", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return Sticker.de_list(result, self) # type: ignore[return-value, arg-type] + + @_log + async def create_forum_topic( + self, + chat_id: Union[str, int], + name: str, + icon_color: int, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to create a topic in a forum supergroup chat. The bot must be + an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. seealso:: :meth:`telegram.Message.create_forum_topic`, + :meth:`telegram.Chat.create_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + name (:obj:`str`): New topic name, + :tg-const:`telegram.constants.TopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.TopicLimit.MAX_NAME_LENGTH` characters. + icon_color (:obj:`int`): Color of the topic icon in RGB format. Currently, + must be one of :attr:`telegram.constants.ForumIconColor.BLUE`, + :attr:`telegram.constants.ForumIconColor.YELLOW`, + :attr:`telegram.constants.ForumIconColor.PURPLE`, + :attr:`telegram.constants.ForumIconColor.GREEN`, + :attr:`telegram.constants.ForumIconColor.PINK`, or + :attr:`telegram.constants.ForumIconColor.RED`. + icon_custom_emoji_id (:obj:`str`): New unique identifier of the custom emoji shown as + the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` to get all + allowed custom emoji identifiers. + + Returns: + :obj:`bool`: On success, :class:`telegram.ForumTopic` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "name": name, + "icon_color": icon_color, + "icon_custom_emoji_id": icon_custom_emoji_id, + } + # TODO: DO ForumTopic.de_json here! + return await self._post( # type: ignore[return-value] + "createForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def edit_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.edit_forum_topic`, + :meth:`telegram.Chat.edit_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + name (:obj:`str`): New topic name, + :tg-const:`telegram.constants.TopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.TopicLimit.MAX_NAME_LENGTH` characters. + icon_custom_emoji_id (:obj:`str`): New unique identifier of the custom emoji shown as + the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` to get all + allowed custom emoji identifiers. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + "name": name, + "icon_custom_emoji_id": icon_custom_emoji_id, + } + return await self._post( # type: ignore[return-value] + "editForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def close_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to close an open topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.close_forum_topic`, + :meth:`telegram.Chat.close_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "closeForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def reopen_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to reopen a closed topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.reopen_forum_topic`, + :meth:`telegram.Chat.reopen_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "reopenForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def delete_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to delete a forum topic along with all its messages in a forum supergroup + chat. The bot must be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights, + unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.delete_forum_topic`, + :meth:`telegram.Chat.delete_forum_topic`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "deleteForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def unpin_all_forum_topic_messages( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """ + Use this method to clear the list of pinned messages in a forum topic. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights + in the supergroup, unless it is the creator of the topic. + + .. seealso:: :meth:`telegram.Message.unpin_all_forum_topic_messages`, + :meth:`telegram.Chat.unpin_all_forum_topic_messages`, + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( # type: ignore[return-value] + "unpinAllForumTopicMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # skipcq: PYL-W0613 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -7070,3 +7415,17 @@ def __hash__(self) -> int: """Alias for :meth:`set_my_default_administrator_rights`""" createInvoiceLink = create_invoice_link """Alias for :meth:`create_invoice_link`""" + getForumTopicIconStickers = get_forum_topic_icon_stickers + """Alias for :meth:`get_forum_topic_icon_stickers`""" + createForumTopic = create_forum_topic + """Alias for :meth:`create_forum_topic`""" + editForumTopic = edit_forum_topic + """Alias for :meth:`edit_forum_topic`""" + closeForumTopic = close_forum_topic + """Alias for :meth:`close_forum_topic`""" + reopenForumTopic = reopen_forum_topic + """Alias for :meth:`reopen_forum_topic`""" + deleteForumTopic = delete_forum_topic + """Alias for :meth:`delete_forum_topic`""" + unpinAllForumTopicMessages = unpin_all_forum_topic_messages + """Alias for :meth:`unpin_all_forum_topic_messages`""" diff --git a/telegram/_chat.py b/telegram/_chat.py index 85a51e0aece..91a751c53aa 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -2617,6 +2617,207 @@ async def set_menu_button( api_kwargs=api_kwargs, ) + async def create_forum_topic( + self, + name: str, + icon_color: int, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.create_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().create_forum_topic( + chat_id=self.id, + name=name, + icon_color=icon_color, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_forum_topic( + self, + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.edit_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_forum_topic( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.close_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_forum_topic( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.reopen_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_forum_topic( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.delete_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_forum_topic_messages( + self, + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.unpin_all_forum_topic_messages(chat_id=update.effective_chat.id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_forum_topic_messages`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unpin_all_forum_topic_messages( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def get_menu_button( self, *, diff --git a/telegram/_message.py b/telegram/_message.py index e9d76b2ced4..f18e00973ea 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -2712,6 +2712,180 @@ async def unpin( api_kwargs=api_kwargs, ) + async def edit_forum_topic( + self, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.edit_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.close_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.reopen_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_forum_topic( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.delete_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_forum_topic_messages( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + await bot.unpin_all_forum_topic_messages( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_forum_topic_messages`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unpin_all_forum_topic_messages( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + def parse_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/telegram/constants.py b/telegram/constants.py index b20f847dc52..1a5bb3e9405 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -46,6 +46,7 @@ "DiceEmoji", "FileSizeLimit", "FloodLimit", + "ForumIconColor", "InlineKeyboardMarkupLimit", "InlineQueryLimit", "InlineQueryResultType", @@ -63,6 +64,7 @@ "PollType", "SUPPORTED_WEBHOOK_PORTS", "StickerType", + "TopicLimit", "WebhookLimit", "UpdateType", ] @@ -371,6 +373,66 @@ class FloodLimit(IntEnum): """ +class ForumIconColor(IntEnum): + """This enum contains the available colors for use in + :paramref:`telegram.Bot.create_forum_topic.icon_color`. The enum members of this enumeration + are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + BLUE = 0x6FB9F0 + """:obj:`int`: An icon with a color which corresponds to blue. + + .. raw:: html + +
+ + """ + YELLOW = 0xFFD67E + """:obj:`int`: An icon with a color which corresponds to yellow. + + .. raw:: html + +
+ + """ + PURPLE = 0xCB86DB + """:obj:`int`: An icon with a color which corresponds to purple. + + .. raw:: html + +
+ + """ + GREEN = 0x8EEE98 + """:obj:`int`: An icon with a color which corresponds to green. + + .. raw:: html + +
+ + """ + PINK = 0xFF93B2 + """:obj:`int`: An icon with a color which corresponds to pink. + + .. raw:: html + +
+ + """ + RED = 0xFB6F5F + """:obj:`int`: An icon with a color which corresponds to red. + + .. raw:: html + +
+ + """ + + class InlineKeyboardMarkupLimit(IntEnum): """This enum contains limitations for :class:`telegram.InlineKeyboardMarkup`/ :meth:`telegram.Bot.send_message` & friends. The enum @@ -917,3 +979,18 @@ class ChatLimit(IntEnum): """:obj:`int`: Minimum length of a new chat title.""" MAX_TITLE_LENGTH = 128 """:obj:`int`: Maximum length of a new chat title.""" + + +class TopicLimit(IntEnum): + """This enum contains limitations for :paramref:`telegram.Bot.edit_forum_topic.name`. The + enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_NAME_LENGTH = 1 + """:obj:`int`: Minimum length of the topic name.""" + MAX_NAME_LENGTH = 128 + """:obj:`int`: Maximum length of the topic name.""" diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 7445c0bffd4..0ae8f88bcac 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -1107,6 +1107,28 @@ async def delete_chat_sticker_set( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def delete_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().delete_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def delete_message( self, chat_id: Union[str, int], @@ -1221,6 +1243,32 @@ async def edit_chat_invite_link( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def edit_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().edit_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def edit_message_caption( self, chat_id: Union[str, int] = None, @@ -1533,6 +1581,24 @@ async def get_file( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_forum_topic_icon_stickers( + self, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> List[Sticker]: + return await super().get_forum_topic_icon_stickers( + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def get_game_high_scores( self, user_id: Union[int, str], @@ -1757,6 +1823,54 @@ async def close( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def close_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().close_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def create_forum_topic( + self, + chat_id: Union[str, int], + name: str, + icon_color: int, + icon_custom_emoji_id: str, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().create_forum_topic( + chat_id=chat_id, + name=name, + icon_color=icon_color, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def pin_chat_message( self, chat_id: Union[str, int], @@ -1827,6 +1941,28 @@ async def promote_chat_member( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def reopen_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().reopen_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def restrict_chat_member( self, chat_id: Union[str, int], @@ -3121,6 +3257,28 @@ async def unpin_chat_message( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def unpin_all_forum_topic_messages( + self, + chat_id: Union[str, int], + message_thread_id: int, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().unpin_all_forum_topic_messages( + chat_id=chat_id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def upload_sticker_file( self, user_id: Union[str, int], @@ -3232,3 +3390,10 @@ async def upload_sticker_file( getMyDefaultAdministratorRights = get_my_default_administrator_rights setMyDefaultAdministratorRights = set_my_default_administrator_rights createInvoiceLink = create_invoice_link + getForumTopicIconStickers = get_forum_topic_icon_stickers + createForumTopic = create_forum_topic + editForumTopic = edit_forum_topic + closeForumTopic = close_forum_topic + reopenForumTopic = reopen_forum_topic + deleteForumTopic = delete_forum_topic + unpinAllForumTopicMessages = unpin_all_forum_topic_messages diff --git a/tests/test_bot.py b/tests/test_bot.py index 85f9c59444b..8a40bc086cd 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2568,6 +2568,9 @@ async def test_pin_and_unpin_message(self, bot, super_group_id): # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers # are tested in the test_sticker module. + # get_forum_topic_icon_stickers, edit_forum_topic, etc... + # are tested in the test_forum module. + async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): # Use BaseException that's not a subclass of Exception such that # OkException should not be caught anywhere diff --git a/tests/test_chat.py b/tests/test_chat.py index 4f2305180b8..e8871deeedd 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -896,6 +896,128 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "decline_chat_join_request", make_assertion) assert await chat.decline_join_request(user_id=42) + async def test_create_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["name"] == "New Name" + and kwargs["icon_color"] == 0x6FB9F0 + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Chat.create_forum_topic, Bot.create_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.create_forum_topic, + chat.get_bot(), + "create_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.create_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "create_forum_topic", make_assertion) + assert await chat.create_forum_topic( + name="New Name", icon_color=0x6FB9F0, icon_custom_emoji_id="12345" + ) + + async def test_edit_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["message_thread_id"] == 42 + and kwargs["name"] == "New Name" + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Chat.edit_forum_topic, Bot.edit_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.edit_forum_topic, chat.get_bot(), "edit_forum_topic", shortcut_kwargs=["chat_id"] + ) + assert await check_defaults_handling(chat.edit_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_forum_topic", make_assertion) + assert await chat.edit_forum_topic( + message_thread_id=42, name="New Name", icon_custom_emoji_id="12345" + ) + + async def test_close_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.close_forum_topic, Bot.close_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.close_forum_topic, + chat.get_bot(), + "close_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.close_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "close_forum_topic", make_assertion) + assert await chat.close_forum_topic(message_thread_id=42) + + async def test_reopen_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.reopen_forum_topic, Bot.reopen_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.reopen_forum_topic, + chat.get_bot(), + "reopen_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.reopen_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "reopen_forum_topic", make_assertion) + assert await chat.reopen_forum_topic(message_thread_id=42) + + async def test_delete_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.delete_forum_topic, Bot.delete_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.delete_forum_topic, + chat.get_bot(), + "delete_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.delete_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_forum_topic", make_assertion) + assert await chat.delete_forum_topic(message_thread_id=42) + + async def test_unpin_all_forum_topic_messages(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.unpin_all_forum_topic_messages, + Bot.unpin_all_forum_topic_messages, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.unpin_all_forum_topic_messages, + chat.get_bot(), + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.unpin_all_forum_topic_messages, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unpin_all_forum_topic_messages", make_assertion) + assert await chat.unpin_all_forum_topic_messages(message_thread_id=42) + def test_mention_html(self): with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): chat = Chat(id=1, type="foo") diff --git a/tests/test_message.py b/tests/test_message.py index 3ad03e386cc..bb8aa8519c6 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -198,6 +198,7 @@ def message(bot): ] }, {"web_app_data": WebAppData("some_data", "some_button_text")}, + {"message_thread_id": 123}, ], ids=[ "forwarded_user", @@ -251,6 +252,7 @@ def message(bot): "has_protected_content", "entities", "web_app_data", + "message_thread_id", ], ) def message_params(bot, request): @@ -1693,6 +1695,124 @@ def test_default_quote(self, message): finally: message.get_bot()._defaults = None + async def test_edit_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + and kwargs["name"] == "New Name" + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Message.edit_forum_topic, Bot.edit_forum_topic, ["chat_id", "message_thread_id"], [] + ) + assert await check_shortcut_call( + message.edit_forum_topic, + message.get_bot(), + "edit_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.edit_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_forum_topic", make_assertion) + assert await message.edit_forum_topic(name="New Name", icon_custom_emoji_id="12345") + + async def test_close_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.close_forum_topic, Bot.close_forum_topic, ["chat_id", "message_thread_id"], [] + ) + assert await check_shortcut_call( + message.close_forum_topic, + message.get_bot(), + "close_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.close_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "close_forum_topic", make_assertion) + assert await message.close_forum_topic() + + async def test_reopen_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.reopen_forum_topic, + Bot.reopen_forum_topic, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.reopen_forum_topic, + message.get_bot(), + "reopen_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.reopen_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "reopen_forum_topic", make_assertion) + assert await message.reopen_forum_topic() + + async def test_delete_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.delete_forum_topic, + Bot.delete_forum_topic, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.delete_forum_topic, + message.get_bot(), + "delete_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.delete_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "delete_forum_topic", make_assertion) + assert await message.delete_forum_topic() + + async def test_unpin_all_forum_topic_messages(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.unpin_all_forum_topic_messages, + Bot.unpin_all_forum_topic_messages, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.unpin_all_forum_topic_messages, + message.get_bot(), + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling( + message.unpin_all_forum_topic_messages, message.get_bot() + ) + + monkeypatch.setattr(message.get_bot(), "unpin_all_forum_topic_messages", make_assertion) + assert await message.unpin_all_forum_topic_messages() + def test_equality(self): id_ = 1 a = Message( From 3940b531105d6396f368c4959eebee04f47e9931 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:57:33 +0100 Subject: [PATCH 13/29] small doc fix --- telegram/_bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index d8825ef4881..f59a8a73036 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -6929,8 +6929,7 @@ async def create_forum_topic( an administrator in the chat for this to work and must have :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. - .. seealso:: :meth:`telegram.Message.create_forum_topic`, - :meth:`telegram.Chat.create_forum_topic`, + .. seealso:: :meth:`telegram.Chat.create_forum_topic`, .. versionadded:: 20.0 From 0cc1d036383288eaa09663a7e1f4329e4bf96c31 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Fri, 18 Nov 2022 18:57:27 +0300 Subject: [PATCH 14/29] New classes for API 6.3: `ForumTopic`, `ForumTopicCreated`, `ForumTopicClosed`, `ForumTopicReopened` (#3356) Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- docs/source/telegram.at-tree.rst | 4 + docs/source/telegram.forumtopic.rst | 6 + docs/source/telegram.forumtopicclosed.rst | 6 + docs/source/telegram.forumtopiccreated.rst | 6 + docs/source/telegram.forumtopicreopened.rst | 6 + telegram/__init__.py | 5 + telegram/_bot.py | 21 +- telegram/_chat.py | 4 +- telegram/_forumtopic.py | 131 ++++++++ telegram/_message.py | 45 ++- telegram/ext/_extbot.py | 7 +- telegram/ext/filters.py | 39 +++ tests/test_bot.py | 26 -- tests/test_filters.py | 15 + tests/test_forum.py | 335 ++++++++++++++++++++ 15 files changed, 613 insertions(+), 43 deletions(-) create mode 100644 docs/source/telegram.forumtopic.rst create mode 100644 docs/source/telegram.forumtopicclosed.rst create mode 100644 docs/source/telegram.forumtopiccreated.rst create mode 100644 docs/source/telegram.forumtopicreopened.rst create mode 100644 telegram/_forumtopic.py create mode 100644 tests/test_forum.py diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 4f2a4ec1a97..41fd5e6385a 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -35,6 +35,10 @@ Available Types telegram.document telegram.file telegram.forcereply + telegram.forumtopic + telegram.forumtopicclosed + telegram.forumtopiccreated + telegram.forumtopicreopened telegram.inlinekeyboardbutton telegram.inlinekeyboardmarkup telegram.inputfile diff --git a/docs/source/telegram.forumtopic.rst b/docs/source/telegram.forumtopic.rst new file mode 100644 index 00000000000..a2dd6389c77 --- /dev/null +++ b/docs/source/telegram.forumtopic.rst @@ -0,0 +1,6 @@ +telegram.ForumTopic +=================== + +.. autoclass:: telegram.ForumTopic + :members: + :show-inheritance: diff --git a/docs/source/telegram.forumtopicclosed.rst b/docs/source/telegram.forumtopicclosed.rst new file mode 100644 index 00000000000..198a6737255 --- /dev/null +++ b/docs/source/telegram.forumtopicclosed.rst @@ -0,0 +1,6 @@ +telegram.ForumTopicClosed +========================= + +.. autoclass:: telegram.ForumTopicClosed + :members: + :show-inheritance: diff --git a/docs/source/telegram.forumtopiccreated.rst b/docs/source/telegram.forumtopiccreated.rst new file mode 100644 index 00000000000..f1da6f81d37 --- /dev/null +++ b/docs/source/telegram.forumtopiccreated.rst @@ -0,0 +1,6 @@ +telegram.ForumTopicCreated +========================== + +.. autoclass:: telegram.ForumTopicCreated + :members: + :show-inheritance: diff --git a/docs/source/telegram.forumtopicreopened.rst b/docs/source/telegram.forumtopicreopened.rst new file mode 100644 index 00000000000..b81c395d254 --- /dev/null +++ b/docs/source/telegram.forumtopicreopened.rst @@ -0,0 +1,6 @@ +telegram.ForumTopicReopened +=========================== + +.. autoclass:: telegram.ForumTopicReopened + :members: + :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index 11b5e9b720f..c0230f076c8 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -67,6 +67,10 @@ "File", "FileCredentials", "ForceReply", + "ForumTopic", + "ForumTopicClosed", + "ForumTopicCreated", + "ForumTopicReopened", "Game", "GameHighScore", "helpers", @@ -230,6 +234,7 @@ from ._files.videonote import VideoNote from ._files.voice import Voice from ._forcereply import ForceReply +from ._forumtopic import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened from ._games.callbackgame import CallbackGame from ._games.game import Game from ._games.gamehighscore import GameHighScore diff --git a/telegram/_bot.py b/telegram/_bot.py index f59a8a73036..9f74a6c8ac0 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -74,6 +74,7 @@ from telegram._files.video import Video from telegram._files.videonote import VideoNote from telegram._files.voice import Voice +from telegram._forumtopic import ForumTopic from telegram._games.gamehighscore import GameHighScore from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._menubutton import MenuButton @@ -6915,15 +6916,15 @@ async def create_forum_topic( self, chat_id: Union[str, int], name: str, - icon_color: int, - icon_custom_emoji_id: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, connect_timeout: ODVInput[float] = DEFAULT_NONE, pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, - ) -> bool: + ) -> ForumTopic: """ Use this method to create a topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have @@ -6938,19 +6939,19 @@ async def create_forum_topic( name (:obj:`str`): New topic name, :tg-const:`telegram.constants.TopicLimit.MIN_NAME_LENGTH`- :tg-const:`telegram.constants.TopicLimit.MAX_NAME_LENGTH` characters. - icon_color (:obj:`int`): Color of the topic icon in RGB format. Currently, + icon_color (:obj:`int`, optional): Color of the topic icon in RGB format. Currently, must be one of :attr:`telegram.constants.ForumIconColor.BLUE`, :attr:`telegram.constants.ForumIconColor.YELLOW`, :attr:`telegram.constants.ForumIconColor.PURPLE`, :attr:`telegram.constants.ForumIconColor.GREEN`, :attr:`telegram.constants.ForumIconColor.PINK`, or :attr:`telegram.constants.ForumIconColor.RED`. - icon_custom_emoji_id (:obj:`str`): New unique identifier of the custom emoji shown as - the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` to get all - allowed custom emoji identifiers. + icon_custom_emoji_id (:obj:`str`, optional): New unique identifier of the custom emoji + shown as the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` + to get all allowed custom emoji identifiers. Returns: - :obj:`bool`: On success, :class:`telegram.ForumTopic` is returned. + :class:`telegram.ForumTopic` Raises: :class:`telegram.error.TelegramError` @@ -6961,8 +6962,7 @@ async def create_forum_topic( "icon_color": icon_color, "icon_custom_emoji_id": icon_custom_emoji_id, } - # TODO: DO ForumTopic.de_json here! - return await self._post( # type: ignore[return-value] + result = await self._post( "createForumTopic", data, read_timeout=read_timeout, @@ -6971,6 +6971,7 @@ async def create_forum_topic( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) + return ForumTopic.de_json(result, self) # type: ignore[return-value, arg-type] @_log async def edit_forum_topic( diff --git a/telegram/_chat.py b/telegram/_chat.py index 91a751c53aa..3acf2d9e34c 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -2620,8 +2620,8 @@ async def set_menu_button( async def create_forum_topic( self, name: str, - icon_color: int, - icon_custom_emoji_id: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py new file mode 100644 index 00000000000..68ea478831b --- /dev/null +++ b/telegram/_forumtopic.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains objects related to Telegram forum topics.""" + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class ForumTopic(TelegramObject): + """ + This object represents the content of a service message about a new forum topic created in + the chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_thread_id`, :attr:`name` and :attr:`icon_color` + are equal. + + .. versionadded:: 20.0 + + Args: + message_thread_id (:obj:`int`): Unique identifier of the forum topic + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown + as the topic icon. + + Attributes: + message_thread_id (:obj:`int`): Unique identifier of the forum topic + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown + as the topic icon. + """ + + __slots__ = ("message_thread_id", "name", "icon_color", "icon_custom_emoji_id") + + def __init__( + self, + message_thread_id: int, + name: str, + icon_color: int, + icon_custom_emoji_id: str = None, + *, + api_kwargs: JSONDict = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.message_thread_id = message_thread_id + self.name = name + self.icon_color = icon_color + self.icon_custom_emoji_id = icon_custom_emoji_id + + self._id_attrs = (self.message_thread_id, self.name, self.icon_color) + + +class ForumTopicCreated(TelegramObject): + """ + This object represents the content of a service message about a new forum topic created in + the chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` and :attr:`icon_color` are equal. + + .. versionadded:: 20.0 + + Args: + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown + as the topic icon. + + Attributes: + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown + as the topic icon. + """ + + __slots__ = ("name", "icon_color", "icon_custom_emoji_id") + + def __init__( + self, + name: str, + icon_color: int, + icon_custom_emoji_id: str = None, + *, + api_kwargs: JSONDict = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name = name + self.icon_color = icon_color + self.icon_custom_emoji_id = icon_custom_emoji_id + + self._id_attrs = (self.name, self.icon_color) + + +class ForumTopicClosed(TelegramObject): + """ + This object represents a service message about a forum topic closed in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + +class ForumTopicReopened(TelegramObject): + """ + This object represents a service message about a forum topic reopened in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () diff --git a/telegram/_message.py b/telegram/_message.py index f18e00973ea..4a69d66934a 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -36,6 +36,7 @@ from telegram._files.video import Video from telegram._files.videonote import VideoNote from telegram._files.voice import Voice +from telegram._forumtopic import ForumTopicClosed, ForumTopicCreated, ForumTopicReopened from telegram._games.game import Game from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged @@ -254,6 +255,18 @@ class Message(TelegramObject): message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which the message belongs; for supergroups only. + .. versionadded:: 20.0 + forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message: + forum topic created + + .. versionadded:: 20.0 + forum_topic_closed (:class:`telegram.ForumTopicClosed`, optional): Service message: + forum topic closed + + .. versionadded:: 20.0 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`, optional): Service message: + forum topic reopened + .. versionadded:: 20.0 Attributes: @@ -407,12 +420,24 @@ class Message(TelegramObject): reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are represented as ordinary url buttons. - is_topic_message (:obj:`bool`, optional): True, if the message is sent to a forum topic. + is_topic_message (:obj:`bool`): Optional. True, if the message is sent to a forum topic. .. versionadded:: 20.0 - message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which + message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which the message belongs; for supergroups only. + .. versionadded:: 20.0 + forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message: + forum topic created + + .. versionadded:: 20.0 + forum_topic_closed (:class:`telegram.ForumTopicClosed`): Optional. Service message: + forum topic closed + + .. versionadded:: 20.0 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`): Optional. Service message: + forum topic reopened + .. versionadded:: 20.0 .. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored @@ -483,6 +508,9 @@ class Message(TelegramObject): "web_app_data", "is_topic_message", "message_thread_id", + "forum_topic_created", + "forum_topic_closed", + "forum_topic_reopened", ) def __init__( @@ -548,6 +576,9 @@ def __init__( web_app_data: WebAppData = None, is_topic_message: bool = None, message_thread_id: int = None, + forum_topic_created: ForumTopicCreated = None, + forum_topic_closed: ForumTopicClosed = None, + forum_topic_reopened: ForumTopicReopened = None, *, api_kwargs: JSONDict = None, ): @@ -616,6 +647,9 @@ def __init__( self.web_app_data = web_app_data self.is_topic_message = is_topic_message self.message_thread_id = message_thread_id + self.forum_topic_created = forum_topic_created + self.forum_topic_closed = forum_topic_closed + self.forum_topic_reopened = forum_topic_reopened self._effective_attachment = DEFAULT_NONE @@ -706,6 +740,13 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]: data.get("video_chat_participants_invited"), bot ) data["web_app_data"] = WebAppData.de_json(data.get("web_app_data"), bot) + data["forum_topic_closed"] = ForumTopicClosed.de_json(data.get("forum_topic_closed"), bot) + data["forum_topic_created"] = ForumTopicCreated.de_json( + data.get("forum_topic_created"), bot + ) + data["forum_topic_reopened"] = ForumTopicReopened.de_json( + data.get("forum_topic_reopened"), bot + ) return super().de_json(data=data, bot=bot) diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 0ae8f88bcac..0fd9713068b 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -53,6 +53,7 @@ Contact, Document, File, + ForumTopic, GameHighScore, InlineKeyboardMarkup, InputMedia, @@ -1849,8 +1850,8 @@ async def create_forum_topic( self, chat_id: Union[str, int], name: str, - icon_color: int, - icon_custom_emoji_id: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1858,7 +1859,7 @@ async def create_forum_topic( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, rate_limit_args: RLARGS = None, - ) -> bool: + ) -> ForumTopic: return await super().create_forum_topic( chat_id=chat_id, name=name, diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 5ad236153cd..00f3744a73f 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1705,6 +1705,9 @@ def filter(self, update: Update) -> bool: or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update) or StatusUpdate.WEB_APP_DATA.check_update(update) + or StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) + or StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) + or StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) ) ALL = _All(name="filters.StatusUpdate.ALL") @@ -1893,6 +1896,42 @@ def filter(self, message: Message) -> bool: .. versionadded:: 20.0 """ + class _ForumTopicCreated(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_created) + + FORUM_TOPIC_CREATED = _ForumTopicCreated(name="filters.StatusUpdate.FORUM_TOPIC_CREATED") + """Messages that contain :attr:`telegram.Message.forum_topic_created`. + + .. versionadded:: 20.0 + """ + + class _ForumTopicClosed(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_closed) + + FORUM_TOPIC_CLOSED = _ForumTopicClosed(name="filters.StatusUpdate.FORUM_TOPIC_CLOSED") + """Messages that contain :attr:`telegram.Message.forum_topic_closed`. + + .. versionadded:: 20.0 + """ + + class _ForumTopicReopened(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_reopened) + + FORUM_TOPIC_REOPENED = _ForumTopicReopened(name="filters.StatusUpdate.FORUM_TOPIC_REOPENED") + """Messages that contain :attr:`telegram.Message.forum_topic_reopened`. + + .. versionadded:: 20.0 + """ + class Sticker: """Filters messages which contain a sticker. diff --git a/tests/test_bot.py b/tests/test_bot.py index 8a40bc086cd..501ea6170ef 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2630,32 +2630,6 @@ async def test_send_message_entities(self, bot, chat_id): assert message.text == test_string assert message.entities == entities - @pytest.mark.flaky(3, 1) - async def test_send_message_to_topic(self, bot, forum_group_id): - # TODO rework when new methods and attributes become available - test_string = "Topics are forever" - - result = await bot._post( - "createForumTopic", - {"chat_id": forum_group_id, "name": "Is just a yellow lemon tree"}, - ) - - message_thread_id = result["message_thread_id"] - - message = await bot.send_message( - chat_id=forum_group_id, text=test_string, message_thread_id=message_thread_id - ) - - assert message.text == test_string - assert message.is_topic_message is True - assert message.message_thread_id == message_thread_id - - result = await bot._post( - "deleteForumTopic", - {"chat_id": forum_group_id, "message_thread_id": message_thread_id}, - ) - assert result is True - @pytest.mark.flaky(3, 1) @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) async def test_send_message_default_parse_mode(self, default_bot, chat_id): diff --git a/tests/test_filters.py b/tests/test_filters.py index a3fcb5c3280..e37411a1d01 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -980,6 +980,21 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.WEB_APP_DATA.check_update(update) update.message.web_app_data = None + update.message.forum_topic_created = "topic" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.FORUM_TOPIC_CREATED.check_update(update) + update.message.forum_topic_created = None + + update.message.forum_topic_closed = "topic" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update) + update.message.forum_topic_closed = None + + update.message.forum_topic_reopened = "topic" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update) + update.message.forum_topic_reopened = None + def test_filters_forwarded(self, update): assert not filters.FORWARDED.check_update(update) update.message.forward_date = datetime.datetime.utcnow() diff --git a/tests/test_forum.py b/tests/test_forum.py new file mode 100644 index 00000000000..5487ca7feaa --- /dev/null +++ b/tests/test_forum.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import pytest + +from telegram import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened, Sticker + +TEST_MSG_TEXT = "Topics are forever" +TEST_TOPIC_ICON_COLOR = 0x6FB9F0 +TEST_TOPIC_NAME = "Sad bot true: real stories" + + +@pytest.fixture(scope="module") +async def emoji_id(bot): + emoji_sticker_list = await bot.get_forum_topic_icon_stickers() + first_sticker = emoji_sticker_list[0] + return first_sticker.custom_emoji_id + + +@pytest.fixture +async def forum_topic_object(forum_group_id, emoji_id): + return ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + + +@pytest.fixture +async def real_topic(bot, emoji_id, forum_group_id): + result = await bot.create_forum_topic( + chat_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + + yield result + + result = await bot.delete_forum_topic( + chat_id=forum_group_id, message_thread_id=result.message_thread_id + ) + assert result is True, "Topic was not deleted" + + +class TestForumTopic: + def test_slot_behaviour(self, mro_slots, forum_topic_object): + for attr in forum_topic_object.__slots__: + assert getattr(forum_topic_object, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(forum_topic_object)) == len( + set(mro_slots(forum_topic_object)) + ), "duplicate slot" + + async def test_expected_values(self, emoji_id, forum_group_id, forum_topic_object): + assert forum_topic_object.message_thread_id == forum_group_id + assert forum_topic_object.icon_color == TEST_TOPIC_ICON_COLOR + assert forum_topic_object.name == TEST_TOPIC_NAME + assert forum_topic_object.icon_custom_emoji_id == emoji_id + + def test_de_json(self, bot, emoji_id, forum_group_id): + assert ForumTopic.de_json(None, bot=bot) is None + + json_dict = { + "message_thread_id": forum_group_id, + "name": TEST_TOPIC_NAME, + "icon_color": TEST_TOPIC_ICON_COLOR, + "icon_custom_emoji_id": emoji_id, + } + topic = ForumTopic.de_json(json_dict, bot) + assert topic.api_kwargs == {} + + assert topic.message_thread_id == forum_group_id + assert topic.icon_color == TEST_TOPIC_ICON_COLOR + assert topic.name == TEST_TOPIC_NAME + assert topic.icon_custom_emoji_id == emoji_id + + def test_to_dict(self, emoji_id, forum_group_id, forum_topic_object): + topic_dict = forum_topic_object.to_dict() + + assert isinstance(topic_dict, dict) + assert topic_dict["message_thread_id"] == forum_group_id + assert topic_dict["name"] == TEST_TOPIC_NAME + assert topic_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + assert topic_dict["icon_custom_emoji_id"] == emoji_id + + def test_equality(self, emoji_id, forum_group_id): + a = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + ) + b = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + c = ForumTopic( + message_thread_id=forum_group_id, + name=f"{TEST_TOPIC_NAME}!", + icon_color=TEST_TOPIC_ICON_COLOR, + ) + d = ForumTopic( + message_thread_id=forum_group_id + 1, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + ) + e = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=0xFFD67E, + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + @pytest.mark.flaky(3, 1) + async def test_create_forum_topic(self, real_topic): + # TODO test with str identifier of a chat too? + result = real_topic + assert isinstance(result, ForumTopic) + assert result.name == TEST_TOPIC_NAME + assert result.message_thread_id + assert isinstance(result.icon_color, int) + assert isinstance(result.icon_custom_emoji_id, str) + + async def test_create_forum_topic_with_only_required_args(self, bot, forum_group_id): + result = await bot.create_forum_topic(chat_id=forum_group_id, name=TEST_TOPIC_NAME) + assert isinstance(result, ForumTopic) + assert result.name == TEST_TOPIC_NAME + assert result.message_thread_id + assert isinstance(result.icon_color, int) # color is still there though it was not passed + assert result.icon_custom_emoji_id is None + + result = await bot.delete_forum_topic( + chat_id=forum_group_id, message_thread_id=result.message_thread_id + ) + assert result is True, "Failed to delete forum topic" + + @pytest.mark.flaky(3, 1) + async def test_get_forum_topic_icon_stickers(self, bot): + emoji_sticker_list = await bot.get_forum_topic_icon_stickers() + first_sticker = emoji_sticker_list[0] + + assert first_sticker.emoji == "📰" + assert first_sticker.height == 512 + assert first_sticker.width == 512 + assert first_sticker.is_animated + assert not first_sticker.is_video + assert first_sticker.set_name == "Topics" + assert first_sticker.type == Sticker.CUSTOM_EMOJI + assert first_sticker.thumb.width == 128 + assert first_sticker.thumb.height == 128 + + # The following data of first item returned has changed in the past already, + # so check sizes loosely and ID's only by length of string + assert first_sticker.thumb.file_size in range(2000, 7000) + assert first_sticker.file_size in range(20000, 70000) + assert len(first_sticker.custom_emoji_id) == 19 + assert len(first_sticker.thumb.file_unique_id) == 16 + assert len(first_sticker.file_unique_id) == 15 + + async def test_edit_forum_topic(self, emoji_id, forum_group_id, bot, real_topic): + result = await bot.edit_forum_topic( + chat_id=forum_group_id, + message_thread_id=real_topic.message_thread_id, + name=f"{TEST_TOPIC_NAME}_EDITED", + icon_custom_emoji_id=emoji_id, + ) + assert result is True, "Failed to edit forum topic" + # no way of checking the edited name, just the boolean result + + @pytest.mark.flaky(3, 1) + async def test_send_message_to_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + message = await bot.send_message( + chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id + ) + + assert message.text == TEST_MSG_TEXT + assert message.is_topic_message is True + assert message.message_thread_id == message_thread_id + + async def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + result = await bot.close_forum_topic( + chat_id=forum_group_id, + message_thread_id=message_thread_id, + ) + assert result is True, "Failed to close forum topic" + # bot will still be able to send a message to a closed topic, so can't test anything like + # the inability to post to the topic + + result = await bot.reopen_forum_topic( + chat_id=forum_group_id, + message_thread_id=message_thread_id, + ) + assert result is True, "Failed to reopen forum topic" + + async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + msgs = [ + await ( + await bot.send_message( + chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id + ) + ).pin() + for _ in range(2) + ] + + assert all(msgs) is True, "Message(s) were not pinned" + + # We need 2 or more pinned msgs for this to work, else we get Chat_not_modified error + result = await bot.unpin_all_forum_topic_messages( + chat_id=forum_group_id, message_thread_id=message_thread_id + ) + assert result is True, "Failed to unpin all the messages in forum topic" + + +@pytest.fixture +def topic_created(): + return ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + + +class TestForumTopicCreated: + def test_slot_behaviour(self, topic_created, mro_slots): + for attr in topic_created.__slots__: + assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(topic_created)) == len( + set(mro_slots(topic_created)) + ), "duplicate slot" + + def test_expected_values(self, topic_created): + assert topic_created.icon_color == TEST_TOPIC_ICON_COLOR + assert topic_created.name == TEST_TOPIC_NAME + + def test_de_json(self, bot): + assert ForumTopicCreated.de_json(None, bot=bot) is None + + json_dict = {"icon_color": TEST_TOPIC_ICON_COLOR, "name": TEST_TOPIC_NAME} + action = ForumTopicCreated.de_json(json_dict, bot) + assert action.api_kwargs == {} + + assert action.icon_color == TEST_TOPIC_ICON_COLOR + assert action.name == TEST_TOPIC_NAME + + def test_to_dict(self, topic_created): + action_dict = topic_created.to_dict() + + assert isinstance(action_dict, dict) + assert action_dict["name"] == TEST_TOPIC_NAME + assert action_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + + def test_equality(self, emoji_id): + a = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + b = ForumTopicCreated( + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + c = ForumTopicCreated(name=f"{TEST_TOPIC_NAME}!", icon_color=TEST_TOPIC_ICON_COLOR) + d = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=0xFFD67E) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestForumTopicClosed: + def test_slot_behaviour(self, mro_slots): + action = ForumTopicClosed() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = ForumTopicClosed.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, ForumTopicClosed) + + def test_to_dict(self): + action = ForumTopicClosed() + action_dict = action.to_dict() + assert action_dict == {} + + +class TestForumTopicReopened: + def test_slot_behaviour(self, mro_slots): + action = ForumTopicReopened() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = ForumTopicReopened.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, ForumTopicReopened) + + def test_to_dict(self): + action = ForumTopicReopened() + action_dict = action.to_dict() + assert action_dict == {} From 86b2a5582ed4bd7ee1573ed87241489f485710d3 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sat, 19 Nov 2022 03:01:46 +0530 Subject: [PATCH 15/29] review + fixes: fix coverage and some docs --- docs/substitutions/global.rst | 2 + telegram/_bot.py | 70 ++++++++++------------------ telegram/_chatadministratorrights.py | 4 +- telegram/_chatmember.py | 10 ++-- telegram/_forumtopic.py | 3 +- telegram/_message.py | 6 ++- tests/test_bot.py | 2 + tests/test_inputmedia.py | 16 +++++++ 8 files changed, 57 insertions(+), 56 deletions(-) diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index b06bb09db41..2d9d486471e 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -28,6 +28,8 @@ .. |message_thread_id| replace:: Unique identifier for the target message thread of the forum topic. +.. |message_thread_id_arg| replace:: Unique identifier for the target message thread (topic) of the forum; for forum supergroups only. + .. |parse_mode| replace:: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. See the constants in :class:`telegram.constants.ParseMode` for the available modes. .. |allow_sending_without_reply| replace:: Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found. diff --git a/telegram/_bot.py b/telegram/_bot.py index 9f74a6c8ac0..7a6aa8cc985 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -699,8 +699,7 @@ async def send_message( :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -831,8 +830,7 @@ async def forward_message( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -912,8 +910,7 @@ async def send_photo( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| @@ -1029,8 +1026,7 @@ async def send_audio( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1161,8 +1157,7 @@ async def send_document( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1267,8 +1262,7 @@ async def send_sticker( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1373,8 +1367,7 @@ async def send_video( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1503,8 +1496,7 @@ async def send_video_note( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1639,8 +1631,7 @@ async def send_animation( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1760,8 +1751,7 @@ async def send_voice( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1858,8 +1848,7 @@ async def send_media_group( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -1993,8 +1982,7 @@ async def send_location( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -2266,8 +2254,7 @@ async def send_venue( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -2387,8 +2374,7 @@ async def send_contact( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -2485,8 +2471,7 @@ async def send_game( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -4256,8 +4241,7 @@ async def send_invoice( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -4629,7 +4613,7 @@ async def promote_chat_member( 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). - can_manage_topics (:obj:`bool`, optional): Pass :obj:`True`, if the administrator is + can_manage_topics (:obj:`bool`, optional): Pass :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. .. versionadded:: 20.0 @@ -5234,8 +5218,8 @@ async def set_chat_title( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| title (:obj:`str`): New chat title, - tg-const:`telegram.constants.ChatLimits.MIN_TITLE_LENGTH`- - tg-const:`telegram.constants.ChatLimits.MAX_TITLE_LENGTH` characters. + :tg-const:`telegram.constants.ChatLimit.MIN_TITLE_LENGTH`- + :tg-const:`telegram.constants.ChatLimit.MAX_TITLE_LENGTH` characters. Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -6033,8 +6017,7 @@ async def send_poll( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -6189,8 +6172,7 @@ async def send_dice( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -6602,8 +6584,7 @@ async def copy_message( protect_content (:obj:`bool`, optional): |protect_content| .. versionadded:: 13.10 - message_thread_id (:obj:`int`, optional): Unique identifier for the target message - thread (topic) of the forum; for forum supergroups only. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| .. versionadded:: 20.0 @@ -6668,7 +6649,7 @@ async def set_chat_menu_button( button. .. seealso:: :meth:`get_chat_menu_button`, :meth:`telegram.Chat.set_menu_button`, - :meth:`telegram.Chat.get_menu_button`, meth:`telegram.User.set_menu_button`, + :meth:`telegram.Chat.get_menu_button`, :meth:`telegram.User.set_menu_button`, :meth:`telegram.User.get_menu_button` .. versionadded:: 20.0 @@ -7142,8 +7123,7 @@ async def delete_forum_topic( """ Use this method to delete a forum topic along with all its messages in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have - :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights, - unless it is the creator of the topic. + :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights. .. seealso:: :meth:`telegram.Message.delete_forum_topic`, :meth:`telegram.Chat.delete_forum_topic`, @@ -7191,7 +7171,7 @@ async def unpin_all_forum_topic_messages( Use this method to clear the list of pinned messages in a forum topic. The bot must be an administrator in the chat for this to work and must have :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights - in the supergroup, unless it is the creator of the topic. + in the supergroup. .. seealso:: :meth:`telegram.Message.unpin_all_forum_topic_messages`, :meth:`telegram.Chat.unpin_all_forum_topic_messages`, diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py index 877bd279f66..32175cba586 100644 --- a/telegram/_chatadministratorrights.py +++ b/telegram/_chatadministratorrights.py @@ -98,10 +98,10 @@ class ChatAdministratorRights(TelegramObject): messages of other users. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_manage_topics (:obj:`bool`) Optional. :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only. - .. versionadded:: 20.0 + .. versionadded:: 20.0 """ __slots__ = ( diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index fd7da6e6f2c..a1db9db3666 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -250,7 +250,7 @@ class ChatMemberAdministrator(ChatMember): messages; channels only. can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin messages; groups and supergroups only. - can_manage_topics (:obj:`bool`) Optional. :obj:`True`, if the user is allowed + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to create, rename, close, and reopen forum topics; supergroups only .. versionadded:: 20.0 @@ -369,8 +369,8 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. - can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed - to create, rename, close, and reopen forum topics; supergroups only + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions @@ -398,8 +398,8 @@ class ChatMemberRestricted(ChatMember): to send animations, games, stickers and use inline bots. can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is allowed to add web page previews to their messages. - can_manage_topics (:obj:`bool`) Optional. :obj:`True`, if the user is allowed - to create, rename, close, and reopen forum topics; supergroups only + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. .. versionadded:: 20.0 until_date (:class:`datetime.datetime`): Date when restrictions diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py index 68ea478831b..3cf3fe55be7 100644 --- a/telegram/_forumtopic.py +++ b/telegram/_forumtopic.py @@ -24,8 +24,7 @@ class ForumTopic(TelegramObject): """ - This object represents the content of a service message about a new forum topic created in - the chat. + This object represents a forum topic. Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`message_thread_id`, :attr:`name` and :attr:`icon_color` diff --git a/telegram/_message.py b/telegram/_message.py index 4a69d66934a..3765566a023 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -249,7 +249,8 @@ class Message(TelegramObject): reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are represented as ordinary url buttons. - is_topic_message (:obj:`bool`, optional): True, if the message is sent to a forum topic. + is_topic_message (:obj:`bool`, optional): :obj:`True`, if the message is sent to a forum + topic. .. versionadded:: 20.0 message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which @@ -420,7 +421,8 @@ class Message(TelegramObject): reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are represented as ordinary url buttons. - is_topic_message (:obj:`bool`): Optional. True, if the message is sent to a forum topic. + is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a forum + topic. .. versionadded:: 20.0 message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which diff --git a/tests/test_bot.py b/tests/test_bot.py index 501ea6170ef..e042f67bbd3 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2829,6 +2829,7 @@ async def post(url, request_data: RequestData, *args, **kwargs): data["caption_entities"] == [MessageEntity(MessageEntity.BOLD, 0, 4).to_dict()], data["protect_content"] is True, + data["message_thread_id"] == 1, ] ): pytest.fail("I got wrong parameters in post") @@ -2846,6 +2847,7 @@ async def post(url, request_data: RequestData, *args, **kwargs): reply_markup=keyboard.to_json() if json_keyboard else keyboard, disable_notification=True, protect_content=True, + message_thread_id=1, ) @pytest.mark.flaky(3, 1) diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index 897a9ebcd5e..2f261d19068 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -45,6 +45,9 @@ # noinspection PyUnresolvedReferences from .test_document import document, document_file # noqa: F401 +# noinspection PyUnresolvedReferences +from .test_forum import emoji_id, real_topic # noqa: F401 + # noinspection PyUnresolvedReferences from .test_photo import _photo, photo, photo_file, thumb # noqa: F401 @@ -465,6 +468,19 @@ async def test_send_media_group_photo(self, bot, chat_id, media_group): mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages ) + async def test_send_media_group_with_message_thread_id( + self, bot, real_topic, forum_group_id, media_group # noqa: F811 + ): + messages = await bot.send_media_group( + forum_group_id, + media_group, + message_thread_id=real_topic.message_thread_id, + ) + assert isinstance(messages, list) + assert len(messages) == 3 + assert all(isinstance(mes, Message) for mes in messages) + assert all(i.message_thread_id == real_topic.message_thread_id for i in messages) + async def test_send_media_group_throws_error_with_group_caption_and_individual_captions( self, bot, From 1d7841031133b0800aa4ae1dfa2ef3ed3554a8a8 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sat, 19 Nov 2022 03:26:13 +0530 Subject: [PATCH 16/29] update bot api version number --- README.rst | 2 +- README_RAW.rst | 2 +- telegram/constants.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 021e267e04a..542c80ae450 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions diff --git a/README_RAW.rst b/README_RAW.rst index 69d9642eb66..e71a2095a9c 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions diff --git a/telegram/constants.py b/telegram/constants.py index 1a5bb3e9405..647fabbe03e 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -100,7 +100,7 @@ def __str__(self) -> str: #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=2) +BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=3) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. From 8afae2dc46d4b689506aa72d27ceaab55fa2e709 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sat, 19 Nov 2022 03:27:09 +0530 Subject: [PATCH 17/29] add xfail marker to test --- tests/test_forum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_forum.py b/tests/test_forum.py index 5487ca7feaa..f878d63e3da 100644 --- a/tests/test_forum.py +++ b/tests/test_forum.py @@ -224,6 +224,7 @@ async def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topi ) assert result is True, "Failed to reopen forum topic" + @pytest.mark.xfail(reason="Can fail due to race conditions in GH actions CI") async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic): message_thread_id = real_topic.message_thread_id From 84a1693db840ca36b574cf00ed1c67444f4a88f5 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sat, 19 Nov 2022 03:27:57 +0530 Subject: [PATCH 18/29] revert changes to test config --- .github/workflows/test.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef168644966..e1809bc7acb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,6 @@ on: pull_request: branches: - master - - api_6.3_update push: branches: - master @@ -46,36 +45,35 @@ jobs: # Test without passport pytest -v --cov -k test_no_passport.py status=$? - + # test without pytz pytest -v --cov --cov-append -k test_datetime.py status=$(( $? > status ? $? : status)) pytest -v --cov --cov-append -k test_defaults.py status=$(( $? > status ? $? : status)) - + # test without pytz & jobqueue pytest -v --cov --cov-append -k test_jobqueue.py pytest -v --cov --cov-append -k test_applicationbuilder.py status=$(( $? > status ? $? : status)) - + # Test without ratelimiter pytest -v --cov --cov-append -k test_ratelimiter.py status=$(( $? > status ? $? : status)) - + # Test without webhooks pytest -v --cov --cov-append -k test_updater.py status=$(( $? > status ? $? : status)) - + # Test without callback-data pytest -v --cov --cov-append -k test_callbackdatacache.py status=$(( $? > status ? $? : status)) - + # Test without socks pytest -v --cov --cov-append -k test_request.py status=$(( $? > status ? $? : status)) - + # Test the rest - export TEST_WITH_OPT_DEPS='true' pip install -r requirements-opts.txt # `-n auto --dist loadfile` uses pytest-xdist to run each test file on a different CPU From fa917c8eac3d3da96bb2b1a7afee7619f9f89686 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sat, 19 Nov 2022 13:16:24 +0300 Subject: [PATCH 19/29] minor(`test_forum.py`): remove TODO the TODO was asking about whether we should test a `str` identifier of a chat (right now only `int` is tested) It was agreed that additional tests were not needed and we will trust Telegram's behaviour --- tests/test_forum.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_forum.py b/tests/test_forum.py index f878d63e3da..d903d3e3d1a 100644 --- a/tests/test_forum.py +++ b/tests/test_forum.py @@ -141,7 +141,6 @@ def test_equality(self, emoji_id, forum_group_id): @pytest.mark.flaky(3, 1) async def test_create_forum_topic(self, real_topic): - # TODO test with str identifier of a chat too? result = real_topic assert isinstance(result, ForumTopic) assert result.name == TEST_TOPIC_NAME From eef0a48762162a2f373847777b4320c88d7d67fb Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sat, 19 Nov 2022 15:01:15 +0300 Subject: [PATCH 20/29] minor(`ChatPermissions`) move "and" in docstring --- telegram/_chatpermissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py index 03c8bde8bde..7a6902a3d17 100644 --- a/telegram/_chatpermissions.py +++ b/telegram/_chatpermissions.py @@ -28,7 +28,7 @@ class ChatPermissions(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`, :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`, - :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages`, + :attr:`can_change_info`, :attr:`can_invite_users` :attr:`can_pin_messages`, and :attr:`can_manage_topics` are equal. .. versionchanged:: 20.0 From 330e4837f5d284f8efadd30b78bad766755d6bc1 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sat, 19 Nov 2022 15:06:56 +0300 Subject: [PATCH 21/29] minor(`ChatPermissions`) add comma before attr in docstring --- telegram/_chatpermissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py index 7a6902a3d17..c05cfacbdd4 100644 --- a/telegram/_chatpermissions.py +++ b/telegram/_chatpermissions.py @@ -28,7 +28,7 @@ class ChatPermissions(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`, :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`, - :attr:`can_change_info`, :attr:`can_invite_users` :attr:`can_pin_messages`, and + :attr:`can_change_info`, :attr:`can_invite_users`, :attr:`can_pin_messages`, and :attr:`can_manage_topics` are equal. .. versionchanged:: 20.0 From e63eae3c74ab49bd02ffd3969e3b77ddfa5ec3f1 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sat, 19 Nov 2022 16:22:31 +0300 Subject: [PATCH 22/29] minor(`constants.ForumIconColor`) add HEX equivalents in docstr colors are automatically converted to `int`. User should be able to see that we mean HEX colors listed in Telegram API --- telegram/constants.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/telegram/constants.py b/telegram/constants.py index 647fabbe03e..4768cdeadf1 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -384,7 +384,7 @@ class ForumIconColor(IntEnum): __slots__ = () BLUE = 0x6FB9F0 - """:obj:`int`: An icon with a color which corresponds to blue. + """:obj:`int`: An icon with a color which corresponds to blue (``0x6FB9F0``). .. raw:: html @@ -392,7 +392,7 @@ class ForumIconColor(IntEnum): """ YELLOW = 0xFFD67E - """:obj:`int`: An icon with a color which corresponds to yellow. + """:obj:`int`: An icon with a color which corresponds to yellow (``0xFFD67E``). .. raw:: html @@ -400,7 +400,7 @@ class ForumIconColor(IntEnum): """ PURPLE = 0xCB86DB - """:obj:`int`: An icon with a color which corresponds to purple. + """:obj:`int`: An icon with a color which corresponds to purple (``0xCB86DB``). .. raw:: html @@ -408,7 +408,7 @@ class ForumIconColor(IntEnum): """ GREEN = 0x8EEE98 - """:obj:`int`: An icon with a color which corresponds to green. + """:obj:`int`: An icon with a color which corresponds to green (``0x8EEE98``). .. raw:: html @@ -416,7 +416,7 @@ class ForumIconColor(IntEnum): """ PINK = 0xFF93B2 - """:obj:`int`: An icon with a color which corresponds to pink. + """:obj:`int`: An icon with a color which corresponds to pink (``0xFF93B2``). .. raw:: html @@ -424,7 +424,7 @@ class ForumIconColor(IntEnum): """ RED = 0xFB6F5F - """:obj:`int`: An icon with a color which corresponds to red. + """:obj:`int`: An icon with a color which corresponds to red (``0xFB6F5F``). .. raw:: html From a0f5e9330c1674dac930f8c6301dbf92857679f1 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sun, 20 Nov 2022 15:12:10 +0300 Subject: [PATCH 23/29] refactor(constants) `ChatLimit` -> `TitleLimit` --- telegram/_bot.py | 4 ++-- telegram/constants.py | 45 ++++++++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 7a6aa8cc985..9fc4f8def9d 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -5218,8 +5218,8 @@ async def set_chat_title( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| title (:obj:`str`): New chat title, - :tg-const:`telegram.constants.ChatLimit.MIN_TITLE_LENGTH`- - :tg-const:`telegram.constants.ChatLimit.MAX_TITLE_LENGTH` characters. + :tg-const:`telegram.constants.TitleLimit.MIN_CHAT_TITLE_LENGTH`- + :tg-const:`telegram.constants.TitleLimit.MAX_CHAT_TITLE_LENGTH` characters. Returns: :obj:`bool`: On success, :obj:`True` is returned. diff --git a/telegram/constants.py b/telegram/constants.py index 4768cdeadf1..67070b8857b 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -39,7 +39,6 @@ "ChatAction", "ChatID", "ChatInviteLinkLimit", - "ChatLimit", "ChatMemberStatus", "ChatType", "CustomEmojiStickerLimit", @@ -64,6 +63,7 @@ "PollType", "SUPPORTED_WEBHOOK_PORTS", "StickerType", + "TitleLimit", "TopicLimit", "WebhookLimit", "UpdateType", @@ -928,6 +928,34 @@ class UpdateType(StringEnum): """:obj:`str`: Updates with :attr:`telegram.Update.chat_join_request`.""" +class TitleLimit(IntEnum): + """This enum contains limitations for + :meth:`telegram.Bot.set_chat_administrator_custom_title` and + :meth:`telegram.Bot.set_chat_title`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + CHAT_ADMINISTRATOR_CUSTOM_TITLE_LENGTH = 16 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_administrator_custom_title.custom_title` parameter of + :meth:`telegram.Bot.set_chat_administrator_custom_title`. + """ + MIN_CHAT_TITLE_LENGTH = 1 + """:obj:`int`: Minimum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + MAX_CHAT_TITLE_LENGTH = 128 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + + class InvoiceLimit(IntEnum): """This enum contains limitations for :meth:`telegram.Bot.create_invoice_link`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. @@ -966,21 +994,6 @@ class WebhookLimit(IntEnum): """:obj:`int`: Maximum length of the secret token.""" -class ChatLimit(IntEnum): - """This enum contains limitations for :paramref:`telegram.Bot.set_chat_title.title`. The - enum members of this enumeration are instances of :class:`int` and can be treated as such. - - .. versionadded:: 20.0 - """ - - __slots__ = () - - MIN_TITLE_LENGTH = 1 - """:obj:`int`: Minimum length of a new chat title.""" - MAX_TITLE_LENGTH = 128 - """:obj:`int`: Maximum length of a new chat title.""" - - class TopicLimit(IntEnum): """This enum contains limitations for :paramref:`telegram.Bot.edit_forum_topic.name`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. From 444e999e3a459889e39e6c0f70936727660f6f42 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sun, 20 Nov 2022 15:20:12 +0300 Subject: [PATCH 24/29] minor(constants) add refs to params in docstrings of `TopicLimit` --- telegram/constants.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/telegram/constants.py b/telegram/constants.py index 67070b8857b..240d6e4ed24 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -995,8 +995,9 @@ class WebhookLimit(IntEnum): class TopicLimit(IntEnum): - """This enum contains limitations for :paramref:`telegram.Bot.edit_forum_topic.name`. The - enum members of this enumeration are instances of :class:`int` and can be treated as such. + """This enum contains limitations for :paramref:`telegram.Bot.create_forum_topic.name` and + :paramref:`telegram.Bot.edit_forum_topic.name`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. .. versionadded:: 20.0 """ @@ -1004,6 +1005,14 @@ class TopicLimit(IntEnum): __slots__ = () MIN_NAME_LENGTH = 1 - """:obj:`int`: Minimum length of the topic name.""" + """:obj:`int`: Minimum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name` + parameter of :meth:`telegram.Bot.edit_forum_topic`. + """ MAX_NAME_LENGTH = 128 - """:obj:`int`: Maximum length of the topic name.""" + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name` + parameter of :meth:`telegram.Bot.edit_forum_topic`. + """ From 937544b4fe3e82be31c9f0e178430b8217544b83 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sun, 20 Nov 2022 20:50:52 +0300 Subject: [PATCH 25/29] minor(constants): rename `TitleLimit`->`ChatLimit`, move it up in code --- telegram/_bot.py | 4 +-- telegram/constants.py | 58 +++++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 9fc4f8def9d..038ecd7c21d 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -5218,8 +5218,8 @@ async def set_chat_title( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| title (:obj:`str`): New chat title, - :tg-const:`telegram.constants.TitleLimit.MIN_CHAT_TITLE_LENGTH`- - :tg-const:`telegram.constants.TitleLimit.MAX_CHAT_TITLE_LENGTH` characters. + :tg-const:`telegram.constants.ChatLimit.MIN_CHAT_TITLE_LENGTH`- + :tg-const:`telegram.constants.ChatLimit.MAX_CHAT_TITLE_LENGTH` characters. Returns: :obj:`bool`: On success, :obj:`True` is returned. diff --git a/telegram/constants.py b/telegram/constants.py index 240d6e4ed24..25ce4f4166d 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -63,7 +63,7 @@ "PollType", "SUPPORTED_WEBHOOK_PORTS", "StickerType", - "TitleLimit", + "ChatLimit", "TopicLimit", "WebhookLimit", "UpdateType", @@ -240,6 +240,34 @@ class ChatInviteLinkLimit(IntEnum): :meth:`telegram.Bot.create_chat_invite_link` and :meth:`telegram.Bot.edit_chat_invite_link`.""" +class ChatLimit(IntEnum): + """This enum contains limitations for + :meth:`telegram.Bot.set_chat_administrator_custom_title` and + :meth:`telegram.Bot.set_chat_title`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + CHAT_ADMINISTRATOR_CUSTOM_TITLE_LENGTH = 16 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_administrator_custom_title.custom_title` parameter of + :meth:`telegram.Bot.set_chat_administrator_custom_title`. + """ + MIN_CHAT_TITLE_LENGTH = 1 + """:obj:`int`: Minimum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + MAX_CHAT_TITLE_LENGTH = 128 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + + class ChatMemberStatus(StringEnum): """This enum contains the available states for :class:`telegram.ChatMember`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. @@ -928,34 +956,6 @@ class UpdateType(StringEnum): """:obj:`str`: Updates with :attr:`telegram.Update.chat_join_request`.""" -class TitleLimit(IntEnum): - """This enum contains limitations for - :meth:`telegram.Bot.set_chat_administrator_custom_title` and - :meth:`telegram.Bot.set_chat_title`. - The enum members of this enumeration are instances of :class:`int` and can be treated as such. - - .. versionadded:: 20.0 - """ - - __slots__ = () - - CHAT_ADMINISTRATOR_CUSTOM_TITLE_LENGTH = 16 - """:obj:`int`: Maximum length of a :obj:`str` passed as the - :paramref:`~telegram.Bot.set_chat_administrator_custom_title.custom_title` parameter of - :meth:`telegram.Bot.set_chat_administrator_custom_title`. - """ - MIN_CHAT_TITLE_LENGTH = 1 - """:obj:`int`: Minimum length of a :obj:`str` passed as the - :paramref:`~telegram.Bot.set_chat_title.title` parameter of - :meth:`telegram.Bot.set_chat_title`. - """ - MAX_CHAT_TITLE_LENGTH = 128 - """:obj:`int`: Maximum length of a :obj:`str` passed as the - :paramref:`~telegram.Bot.set_chat_title.title` parameter of - :meth:`telegram.Bot.set_chat_title`. - """ - - class InvoiceLimit(IntEnum): """This enum contains limitations for :meth:`telegram.Bot.create_invoice_link`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. From 101b8e40791a7614ee2278ae7d60aa4fc2d19d91 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sun, 20 Nov 2022 20:53:35 +0300 Subject: [PATCH 26/29] minor(constants): rename `TopicLimit`->`ForumTopicLimit` --- telegram/_bot.py | 8 ++++---- telegram/constants.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/telegram/_bot.py b/telegram/_bot.py index 038ecd7c21d..5ff220ee99a 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -6918,8 +6918,8 @@ async def create_forum_topic( Args: chat_id (:obj:`int` | :obj:`str`): |chat_id_group| name (:obj:`str`): New topic name, - :tg-const:`telegram.constants.TopicLimit.MIN_NAME_LENGTH`- - :tg-const:`telegram.constants.TopicLimit.MAX_NAME_LENGTH` characters. + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. icon_color (:obj:`int`, optional): Color of the topic icon in RGB format. Currently, must be one of :attr:`telegram.constants.ForumIconColor.BLUE`, :attr:`telegram.constants.ForumIconColor.YELLOW`, @@ -6983,8 +6983,8 @@ async def edit_forum_topic( chat_id (:obj:`int` | :obj:`str`): |chat_id_group| message_thread_id (:obj:`int`): |message_thread_id| name (:obj:`str`): New topic name, - :tg-const:`telegram.constants.TopicLimit.MIN_NAME_LENGTH`- - :tg-const:`telegram.constants.TopicLimit.MAX_NAME_LENGTH` characters. + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. icon_custom_emoji_id (:obj:`str`): New unique identifier of the custom emoji shown as the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` to get all allowed custom emoji identifiers. diff --git a/telegram/constants.py b/telegram/constants.py index 25ce4f4166d..c56aace5d99 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -64,7 +64,7 @@ "SUPPORTED_WEBHOOK_PORTS", "StickerType", "ChatLimit", - "TopicLimit", + "ForumTopicLimit", "WebhookLimit", "UpdateType", ] @@ -994,7 +994,7 @@ class WebhookLimit(IntEnum): """:obj:`int`: Maximum length of the secret token.""" -class TopicLimit(IntEnum): +class ForumTopicLimit(IntEnum): """This enum contains limitations for :paramref:`telegram.Bot.create_forum_topic.name` and :paramref:`telegram.Bot.edit_forum_topic.name`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. From fba54217a65fd4a4c0899341f76a5c75ac8d0a48 Mon Sep 17 00:00:00 2001 From: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com> Date: Sun, 20 Nov 2022 20:56:07 +0300 Subject: [PATCH 27/29] minor(constants): sort `__all__` alphabetically --- telegram/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telegram/constants.py b/telegram/constants.py index c56aace5d99..aba31dc4903 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -39,6 +39,7 @@ "ChatAction", "ChatID", "ChatInviteLinkLimit", + "ChatLimit", "ChatMemberStatus", "ChatType", "CustomEmojiStickerLimit", @@ -46,6 +47,7 @@ "FileSizeLimit", "FloodLimit", "ForumIconColor", + "ForumTopicLimit", "InlineKeyboardMarkupLimit", "InlineQueryLimit", "InlineQueryResultType", @@ -63,8 +65,6 @@ "PollType", "SUPPORTED_WEBHOOK_PORTS", "StickerType", - "ChatLimit", - "ForumTopicLimit", "WebhookLimit", "UpdateType", ] From 2f624e2230ea216c7a69a87d290ec45b05bb40eb Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 20 Nov 2022 19:42:37 +0100 Subject: [PATCH 28/29] `filters.IS_TOPIC_MESSAGE` --- telegram/ext/filters.py | 14 ++++++++++++++ tests/test_filters.py | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 00f3744a73f..2e2aa8a12ea 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1422,6 +1422,20 @@ def filter(self, message: Message) -> bool: """ +class _IsTopicMessage(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.is_topic_message) + + +IS_TOPIC_MESSAGE = _IsTopicMessage(name="filters.IS_TOPIC_MESSAGE") +"""Messages that contain :attr:`telegram.Message.is_topic_message`. + + .. versionadded:: 20.0 +""" + + class Language(MessageFilter): """Filters messages to only allow those which are from users with a certain language code. diff --git a/tests/test_filters.py b/tests/test_filters.py index e37411a1d01..1ca3fb85106 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1779,6 +1779,11 @@ def test_filters_is_automatic_forward(self, update): update.message.is_automatic_forward = True assert filters.IS_AUTOMATIC_FORWARD.check_update(update) + def test_filters_is_topic_message(self, update): + assert not filters.IS_TOPIC_MESSAGE.check_update(update) + update.message.is_topic_message = True + assert filters.IS_TOPIC_MESSAGE.check_update(update) + def test_filters_has_protected_content(self, update): assert not filters.HAS_PROTECTED_CONTENT.check_update(update) update.message.has_protected_content = True From c0f4c2f781869a6f4cf99e626fcdbeec3788ea67 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 22 Nov 2022 10:37:53 +0100 Subject: [PATCH 29/29] Update filters.__all__ and add a unit test --- telegram/ext/filters.py | 1 + tests/test_filters.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 2e2aa8a12ea..cdd4cf62ccc 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -61,6 +61,7 @@ "HAS_PROTECTED_CONTENT", "INVOICE", "IS_AUTOMATIC_FORWARD", + "IS_TOPIC_MESSAGE", "LOCATION", "Language", "MessageFilter", diff --git a/tests/test_filters.py b/tests/test_filters.py index 1ca3fb85106..a79a7be0916 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -126,6 +126,22 @@ def filter_class(obj): for attr in cls.__slots__: assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}' for {name}" + def test__all__(self): + expected = { + key + for key, member in filters.__dict__.items() + if ( + not key.startswith("_") + # exclude imported stuff + and getattr(member, "__module__", "unknown module") == "telegram.ext.filters" + and key != "sys" + ) + } + actual = set(filters.__all__) + assert ( + actual == expected + ), f"Members {expected - actual} were not listed in constants.__all__" + def test_filters_all(self, update): assert filters.ALL.check_update(update)