diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9e03f85dd9f..f5e734e1a2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: args: - --diff - --check -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 diff --git a/README.rst b/README.rst index 01371e8e8cb..6afd0587397 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :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 f6f0ccf9e48..1195d7db941 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :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/docs/source/conf.py b/docs/source/conf.py index a4cf83391c2..8e59434d65b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,6 +13,7 @@ # serve to show the default. import sys import os +from pathlib import Path # import telegram # If extensions (or modules to document with autodoc) are in another directory, @@ -50,6 +51,9 @@ # The master toctree document. master_doc = 'index' +# Global substitutions +rst_prolog = (Path.cwd() / "substitutions/global.rst").read_text(encoding="utf-8") + # General information about the project. project = u'python-telegram-bot' copyright = u'2015-2022, Leandro Toledo' diff --git a/docs/source/substitutions/global.rst b/docs/source/substitutions/global.rst new file mode 100644 index 00000000000..d099ddb4141 --- /dev/null +++ b/docs/source/substitutions/global.rst @@ -0,0 +1,5 @@ +.. |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. + +.. |chat_id_group| replace:: Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``). diff --git a/docs/source/telegram.forumtopic.rst b/docs/source/telegram.forumtopic.rst new file mode 100644 index 00000000000..3cc64758787 --- /dev/null +++ b/docs/source/telegram.forumtopic.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/v13.x/telegram/forumtopic.py + +telegram.ForumTopic +=================== + +.. autoclass:: telegram.ForumTopic + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.forumtopicclosed.rst b/docs/source/telegram.forumtopicclosed.rst new file mode 100644 index 00000000000..af36f7ef1d0 --- /dev/null +++ b/docs/source/telegram.forumtopicclosed.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/v13.x/telegram/forumtopic.py + +telegram.ForumTopicClosed +========================= + +.. autoclass:: telegram.ForumTopicClosed + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.forumtopiccreated.rst b/docs/source/telegram.forumtopiccreated.rst new file mode 100644 index 00000000000..aea401d59df --- /dev/null +++ b/docs/source/telegram.forumtopiccreated.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/v13.x/telegram/forumtopic.py + +telegram.ForumTopicCreated +========================== + +.. autoclass:: telegram.ForumTopicCreated + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.forumtopicreopened.rst b/docs/source/telegram.forumtopicreopened.rst new file mode 100644 index 00000000000..26ead201dd5 --- /dev/null +++ b/docs/source/telegram.forumtopicreopened.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/v13.x/telegram/forumtopic.py + +telegram.ForumTopicReopened +=========================== + +.. autoclass:: telegram.ForumTopicReopened + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.rst b/docs/source/telegram.rst index e8cc2b523d3..2597a57f368 100644 --- a/docs/source/telegram.rst +++ b/docs/source/telegram.rst @@ -39,6 +39,10 @@ telegram package telegram.error telegram.file telegram.forcereply + telegram.forumtopic + telegram.forumtopicclosed + telegram.forumtopiccreated + telegram.forumtopicreopened telegram.inlinekeyboardbutton telegram.inlinekeyboardmarkup telegram.inputfile diff --git a/telegram/__init__.py b/telegram/__init__.py index 9fa017200eb..5102330a023 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -64,6 +64,7 @@ from .replykeyboardmarkup import ReplyKeyboardMarkup from .replykeyboardremove import ReplyKeyboardRemove from .forcereply import ForceReply +from .forumtopic import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened from .error import TelegramError from .files.inputfile import InputFile from .files.file import File @@ -230,6 +231,10 @@ 'File', 'FileCredentials', 'ForceReply', + 'ForumTopic', + 'ForumTopicClosed', + 'ForumTopicCreated', + 'ForumTopicReopened', 'Game', 'GameHighScore', 'IdDocumentData', diff --git a/telegram/bot.py b/telegram/bot.py index af703f81659..276621b8638 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -94,6 +94,7 @@ ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError +from telegram.forumtopic import ForumTopic from telegram.utils.deprecate import TelegramDeprecationWarning from telegram.utils.helpers import ( DEFAULT_NONE, @@ -310,6 +311,7 @@ def _message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Union[bool, Message]: if reply_to_message_id is not None: data['reply_to_message_id'] = reply_to_message_id @@ -317,6 +319,9 @@ def _message( if protect_content: data['protect_content'] = protect_content + if message_thread_id is not None: + data["message_thread_id"] = message_thread_id + # We don't check if (DEFAULT_)None here, so that _put is able to insert the defaults # correctly, if necessary data['disable_notification'] = disable_notification @@ -471,6 +476,7 @@ def send_message( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to send text messages. @@ -492,6 +498,9 @@ def send_message( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 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 @@ -532,6 +541,7 @@ def send_message( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -547,6 +557,8 @@ 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. @@ -590,6 +602,7 @@ def forward_message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to forward messages of any kind. Service messages can't be forwarded. @@ -613,6 +626,9 @@ def forward_message( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of @@ -642,6 +658,7 @@ def forward_message( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -660,6 +677,7 @@ def send_photo( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to send photos. @@ -698,6 +716,9 @@ def send_photo( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -739,6 +760,7 @@ def send_photo( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -761,6 +783,7 @@ def send_audio( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the @@ -809,6 +832,9 @@ def send_audio( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -867,6 +893,7 @@ def send_audio( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -887,6 +914,7 @@ def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send general files. @@ -929,6 +957,9 @@ def send_document( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -983,6 +1014,7 @@ def send_document( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -997,6 +1029,7 @@ def send_sticker( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers. @@ -1023,6 +1056,9 @@ def send_sticker( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1054,6 +1090,7 @@ def send_sticker( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -1077,6 +1114,7 @@ def send_video( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos @@ -1128,6 +1166,9 @@ def send_video( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1187,6 +1228,7 @@ def send_video( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -1205,6 +1247,7 @@ def send_video_note( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. @@ -1243,6 +1286,9 @@ def send_video_note( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1293,6 +1339,7 @@ def send_video_note( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -1315,6 +1362,7 @@ def send_animation( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -1323,7 +1371,7 @@ def send_animation( Note: ``thumb`` will be ignored for small files, for which Telegram can easily - generate thumb nails. However, this behaviour is undocumented and might be changed + generate thumbnails. However, this behaviour is undocumented and might be changed by Telegram. Args: @@ -1369,6 +1417,9 @@ def send_animation( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1417,6 +1468,7 @@ def send_animation( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -1436,6 +1488,7 @@ def send_voice( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file @@ -1479,6 +1532,9 @@ def send_voice( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1522,6 +1578,7 @@ def send_voice( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -1537,6 +1594,7 @@ def send_media_group( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> List[Message]: """Use this method to send a group of photos or videos as an album. @@ -1552,6 +1610,9 @@ def send_media_group( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1587,6 +1648,9 @@ def send_media_group( if protect_content: data['protect_content'] = protect_content + if message_thread_id: + data["message_thread_id"] = message_thread_id + result = self._post('sendMediaGroup', data, timeout=timeout, api_kwargs=api_kwargs) return Message.de_list(result, self) # type: ignore @@ -1609,6 +1673,7 @@ def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to send point on the map. @@ -1636,6 +1701,9 @@ def send_location( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1692,6 +1760,7 @@ def send_location( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -1853,6 +1922,7 @@ def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to send information about a venue. @@ -1886,6 +1956,9 @@ def send_venue( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -1950,6 +2023,7 @@ def send_venue( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -1968,6 +2042,7 @@ def send_contact( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to send phone contacts. @@ -1990,6 +2065,9 @@ def send_contact( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -2043,6 +2121,7 @@ def send_contact( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -2057,6 +2136,7 @@ def send_game( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to send a game. @@ -2070,6 +2150,9 @@ def send_game( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -2103,6 +2186,7 @@ def send_game( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -2246,9 +2330,8 @@ def answer_inline_query( current_offset: str = None, api_kwargs: JSONDict = None, ) -> bool: - """ - Use this method to send answers to an inline query. No more than 50 results per query are - allowed. + """Use this method to send answers to an inline query. No more than 50 results per query + are allowed. Warning: In most use cases :attr:`current_offset` should not be passed manually. Instead of @@ -3627,6 +3710,7 @@ def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """Use this method to send invoices. @@ -3705,6 +3789,9 @@ def send_invoice( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -3779,6 +3866,7 @@ def send_invoice( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -4031,6 +4119,7 @@ def promote_chat_member( can_manage_chat: bool = None, can_manage_voice_chats: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, ) -> bool: """ Use this method to promote or demote a user in a supergroup or a channel. The bot must be @@ -4086,6 +4175,10 @@ def promote_chat_member( the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. + 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:: 13.15 Returns: :obj:`bool`: On success, :obj:`True` is returned. @@ -4125,6 +4218,8 @@ def promote_chat_member( data['can_manage_video_chats'] = can_manage_voice_chats 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 = self._post('promoteChatMember', data, timeout=timeout, api_kwargs=api_kwargs) @@ -5273,6 +5368,7 @@ def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send a native poll. @@ -5315,6 +5411,9 @@ def send_poll( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -5376,6 +5475,7 @@ def send_poll( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -5436,6 +5536,7 @@ def send_dice( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> Message: """ Use this method to send an animated emoji that will display a random value. @@ -5456,6 +5557,9 @@ def send_dice( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -5492,6 +5596,7 @@ def send_dice( allow_sending_without_reply=allow_sending_without_reply, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) @log @@ -5813,6 +5918,7 @@ def copy_message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> MessageId: """ Use this method to copy messages of any kind. Service messages and invoice messages can't @@ -5838,6 +5944,9 @@ def copy_message( forwarding and saving. .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 13.15 reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the original message. @@ -5881,6 +5990,8 @@ def copy_message( data['reply_markup'] = reply_markup.to_json() else: data['reply_markup'] = reply_markup + if message_thread_id: + data["message_thread_id"] = message_thread_id result = self._post('copyMessage', data, timeout=timeout, api_kwargs=api_kwargs) return MessageId.de_json(result, self) # type: ignore[return-value, arg-type] @@ -6096,6 +6207,338 @@ def create_invoice_link( api_kwargs=api_kwargs, ) + @log + def get_forum_topic_icon_stickers( + self, + 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:: 13.15 + + Args: + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + List[:class:`telegram.Sticker`] + + Raises: + :class:`telegram.error.TelegramError` + """ + result = self._post( + "getForumTopicIconStickers", + timeout=timeout, + api_kwargs=api_kwargs, + ) + return Sticker.de_list(result, self) # type: ignore[return-value, arg-type] + + @log + def create_forum_topic( + self, + chat_id: Union[str, int], + name: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> 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 + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. seealso:: :meth:`telegram.Chat.create_forum_topic`, + + .. versionadded:: 13.15 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + name (:obj:`str`): New topic name, 1-128 characters. + icon_color (:obj:`int`, optional): Color of the topic icon in RGB format. Currently, + must be one of 7322096 (0x6FB9F0), 16766590 (0xFFD67E), 13338331 (0xCB86DB), + 9367192 (0x8EEE98), 16749490 (0xFF93B2), or 16478047 (0xFB6F5F) + 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. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`telegram.ForumTopic` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "name": name, + } + + if icon_color is not None: + data["icon_color"] = icon_color + + if icon_custom_emoji_id is not None: + data["icon_custom_emoji_id"] = icon_custom_emoji_id + + result = self._post( + "createForumTopic", + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) + return ForumTopic.de_json(result, self) # type: ignore[return-value, arg-type] + + @log + def edit_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + 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 + :attr:`~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:: 13.15 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + .. versionadded:: 13.15 + name (:obj:`str`): New topic name, 1-128 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. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + 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 self._post( # type: ignore[return-value] + "editForumTopic", + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + @log + def close_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + 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 + :attr:`~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:: 13.15 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + .. versionadded:: 13.15 + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + 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 self._post( # type: ignore[return-value] + "closeForumTopic", + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + @log + def reopen_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + 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 + :meth:`~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:: 13.15 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + .. versionadded:: 13.15 + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + 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 self._post( # type: ignore[return-value] + "reopenForumTopic", + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + @log + def delete_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + 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 + :meth:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights. + + .. seealso:: :meth:`telegram.Message.delete_forum_topic`, + :meth:`telegram.Chat.delete_forum_topic`, + + .. versionadded:: 13.15 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + .. versionadded:: 13.15 + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + 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 self._post( # type: ignore[return-value] + "deleteForumTopic", + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + @log + def unpin_all_forum_topic_messages( + self, + chat_id: Union[str, int], + message_thread_id: int, + 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 + :meth:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights + in the supergroup. + + .. seealso:: :meth:`telegram.Message.unpin_all_forum_topic_messages`, + :meth:`telegram.Chat.unpin_all_forum_topic_messages`, + + .. versionadded:: 13.15 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + .. versionadded:: 13.15 + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + 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 self._post( # type: ignore[return-value] + "unpinAllForumTopicMessages", + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {'id': self.id, 'username': self.username, 'first_name': self.first_name} @@ -6292,3 +6735,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/callbackquery.py b/telegram/callbackquery.py index b783636045f..e4127849eba 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -621,6 +621,7 @@ def copy_message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'MessageId': """Shortcut for:: @@ -650,6 +651,7 @@ def copy_message( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) MAX_ANSWER_TEXT_LENGTH: ClassVar[int] = constants.MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH diff --git a/telegram/chat.py b/telegram/chat.py index 3d6e8178870..46248657381 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -35,6 +35,7 @@ Bot, ChatMember, ChatInviteLink, + ForumTopic, Message, MessageId, ReplyMarkup, @@ -131,6 +132,21 @@ class Chat(TelegramObject): in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.14 + is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 13.15 + 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:: 13.15 + 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:: 13.15 **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -190,6 +206,23 @@ class Chat(TelegramObject): in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.14 + is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 13.15 + 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:: 13.15 + 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:: 13.15 + + .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups """ __slots__ = ( @@ -218,6 +251,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', '_id_attrs', ) @@ -261,6 +297,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, **_kwargs: Any, ): # Required @@ -292,6 +331,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.bot = bot self._id_attrs = (self.id,) @@ -629,6 +671,7 @@ def promote_member( can_manage_chat: bool = None, can_manage_voice_chats: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, ) -> bool: """Shortcut for:: @@ -663,6 +706,7 @@ def promote_member( can_manage_chat=can_manage_chat, can_manage_voice_chats=can_manage_voice_chats, can_manage_video_chats=can_manage_video_chats, + can_manage_topics=can_manage_topics, ) def restrict_member( @@ -836,6 +880,7 @@ def send_message( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -860,6 +905,7 @@ def send_message( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_media_group( @@ -873,6 +919,7 @@ def send_media_group( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> List['Message']: """Shortcut for:: @@ -893,6 +940,7 @@ 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, ) def send_chat_action( @@ -935,6 +983,7 @@ def send_photo( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -960,6 +1009,7 @@ def send_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_contact( @@ -976,6 +1026,7 @@ def send_contact( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1001,6 +1052,7 @@ 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, ) def send_audio( @@ -1021,6 +1073,7 @@ def send_audio( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1050,6 +1103,7 @@ def send_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_document( @@ -1068,6 +1122,7 @@ def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1095,6 +1150,7 @@ 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, ) def send_dice( @@ -1107,6 +1163,7 @@ def send_dice( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1128,6 +1185,7 @@ 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, ) def send_game( @@ -1140,6 +1198,7 @@ def send_game( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1161,6 +1220,7 @@ 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, ) def send_invoice( @@ -1193,6 +1253,7 @@ def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1242,6 +1303,7 @@ 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, ) def send_location( @@ -1260,6 +1322,7 @@ def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1287,6 +1350,7 @@ 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, ) def send_animation( @@ -1307,6 +1371,7 @@ def send_animation( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1336,6 +1401,7 @@ def send_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_sticker( @@ -1348,6 +1414,7 @@ def send_sticker( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1369,6 +1436,7 @@ 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, ) def send_venue( @@ -1389,6 +1457,7 @@ def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1418,6 +1487,7 @@ 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, ) def send_video( @@ -1439,6 +1509,7 @@ def send_video( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1469,6 +1540,7 @@ def send_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_video_note( @@ -1485,6 +1557,7 @@ def send_video_note( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1510,6 +1583,7 @@ def send_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_voice( @@ -1527,6 +1601,7 @@ def send_voice( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1553,6 +1628,7 @@ def send_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_poll( @@ -1577,6 +1653,7 @@ def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1609,6 +1686,7 @@ 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, ) def send_copy( @@ -1625,6 +1703,7 @@ def send_copy( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'MessageId': """Shortcut for:: @@ -1650,6 +1729,7 @@ def send_copy( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def copy_message( @@ -1666,6 +1746,7 @@ def copy_message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'MessageId': """Shortcut for:: @@ -1691,6 +1772,7 @@ def copy_message( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def export_invite_link( @@ -1887,6 +1969,165 @@ def set_menu_button( api_kwargs=api_kwargs, ) + def create_forum_topic( + self, + name: str, + icon_color: int = None, + icon_custom_emoji_id: str = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> "ForumTopic": + """Shortcut for:: + + 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:: 13.15 + + Returns: + :class:`telegram.ForumTopic` + """ + return self.bot.create_forum_topic( + chat_id=self.id, + name=name, + icon_color=icon_color, + icon_custom_emoji_id=icon_custom_emoji_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def edit_forum_topic( + self, + message_thread_id: int, + name: str, + icon_custom_emoji_id: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.edit_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def close_forum_topic( + self, + message_thread_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.close_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def reopen_forum_topic( + self, + message_thread_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.reopen_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def delete_forum_topic( + self, + message_thread_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.delete_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def unpin_all_forum_topic_messages( + self, + message_thread_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.unpin_all_forum_topic_messages( + chat_id=self.id, + message_thread_id=message_thread_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + def get_menu_button( self, timeout: ODVInput[float] = DEFAULT_NONE, diff --git a/telegram/chatadministratorrights.py b/telegram/chatadministratorrights.py index 68db99c736e..f3a373be127 100644 --- a/telegram/chatadministratorrights.py +++ b/telegram/chatadministratorrights.py @@ -62,6 +62,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:: 13.15 Attributes: is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. @@ -89,6 +93,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:: 13.15 """ __slots__ = ( @@ -103,6 +111,7 @@ class ChatAdministratorRights(TelegramObject): 'can_post_messages', 'can_edit_messages', 'can_pin_messages', + 'can_manage_topics', '_id_attrs', ) @@ -119,6 +128,7 @@ def __init__( can_post_messages: bool = None, can_edit_messages: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, **_kwargs: Any, ) -> None: # Required @@ -134,6 +144,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, @@ -156,7 +167,7 @@ def all_rights(cls) -> 'ChatAdministratorRights': :obj:`True`. This is e.g. useful when changing the bot's default administrator rights with :meth:`telegram.Bot.set_my_default_administrator_rights`. """ - 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': @@ -164,4 +175,6 @@ def no_rights(cls) -> 'ChatAdministratorRights': This method returns the :class:`ChatAdministratorRights` object with all attributes set to :obj:`False`. """ - 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 c091fbd42e4..7a1178d60ca 100644 --- a/telegram/chatmember.py +++ b/telegram/chatmember.py @@ -288,6 +288,7 @@ class ChatMember(TelegramObject): 'can_manage_voice_chats', 'can_manage_video_chats', 'until_date', + 'can_manage_topics', '_id_attrs', ) @@ -329,6 +330,7 @@ def __init__( can_manage_chat: bool = None, can_manage_voice_chats: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, **_kwargs: Any, ): # check before required to not waste resources if the error is raised @@ -371,6 +373,7 @@ def __init__( ) self.can_manage_voice_chats = temp self.can_manage_video_chats = temp + self.can_manage_topics = can_manage_topics self._id_attrs = (self.user, self.status) @@ -494,6 +497,10 @@ class ChatMemberAdministrator(ChatMember): new users to the chat. 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:: 13.15 Attributes: status (:obj:`str`): The member's status in the chat, @@ -536,6 +543,10 @@ class ChatMemberAdministrator(ChatMember): new users to the chat. 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:: 13.15 """ __slots__ = () @@ -557,6 +568,7 @@ def __init__( can_invite_users: bool = None, can_pin_messages: bool = None, can_manage_video_chats: bool = None, + can_manage_topics: bool = None, **_kwargs: Any, ): super().__init__( @@ -576,6 +588,7 @@ def __init__( can_invite_users=can_invite_users, can_pin_messages=can_pin_messages, can_manage_video_chats=can_manage_video_chats, + can_manage_topics=can_manage_topics, ) @@ -631,6 +644,10 @@ class ChatMemberRestricted(ChatMember): allowed to add web page previews to their messages. until_date (:class:`datetime.datetime`, optional): Date when restrictions will be lifted for this user. + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. + + .. versionadded:: 13.15 Attributes: status (:obj:`str`): The member's status in the chat, @@ -656,6 +673,10 @@ class ChatMemberRestricted(ChatMember): allowed to add web page previews to their messages. until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted for this user. + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. + + .. versionadded:: 13.15 """ @@ -674,6 +695,7 @@ def __init__( can_send_other_messages: bool = None, can_add_web_page_previews: bool = None, until_date: datetime.datetime = None, + can_manage_topics: bool = None, **_kwargs: Any, ): super().__init__( @@ -689,6 +711,7 @@ def __init__( can_send_other_messages=can_send_other_messages, can_add_web_page_previews=can_add_web_page_previews, until_date=until_date, + can_manage_topics=can_manage_topics, ) diff --git a/telegram/chatpermissions.py b/telegram/chatpermissions.py index 44b989a0013..a2f9be8b841 100644 --- a/telegram/chatpermissions.py +++ b/telegram/chatpermissions.py @@ -34,7 +34,7 @@ class ChatPermissions(TelegramObject): 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 @@ -55,6 +55,11 @@ 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 forum topics. If omitted defaults to the value of + :attr:`can_pin_messages`. + + .. versionadded:: 13.15 Attributes: can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text @@ -75,6 +80,11 @@ 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 forum topics. If omitted defaults to the value of + :attr:`can_pin_messages`. + + .. versionadded:: 13.15 """ @@ -88,6 +98,7 @@ class ChatPermissions(TelegramObject): 'can_change_info', 'can_pin_messages', 'can_add_web_page_previews', + 'can_manage_topics', ) def __init__( @@ -100,6 +111,7 @@ def __init__( can_change_info: bool = None, can_invite_users: bool = None, can_pin_messages: bool = None, + can_manage_topics: bool = None, **_kwargs: Any, ): # Required @@ -111,6 +123,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, diff --git a/telegram/constants.py b/telegram/constants.py index ae5175d9d2f..7eeceeffc3f 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -21,7 +21,7 @@ `Telegram Bots API `_. Attributes: - BOT_API_VERSION (:obj:`str`): `6.2`. Telegram Bot API version supported by this + BOT_API_VERSION (:obj:`str`): `6.3`. Telegram Bot API version supported by this version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. .. versionadded:: 13.4 @@ -263,7 +263,7 @@ """ from typing import List -BOT_API_VERSION: str = '6.2' +BOT_API_VERSION: str = '6.3' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 diff --git a/telegram/ext/extbot.py b/telegram/ext/extbot.py index ff2de0830d7..cd0a3e3af31 100644 --- a/telegram/ext/extbot.py +++ b/telegram/ext/extbot.py @@ -196,6 +196,7 @@ def _message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> Union[bool, Message]: # We override this method to call self._replace_keyboard and self._insert_callback_data. # This covers most methods that have a reply_markup @@ -209,6 +210,7 @@ def _message( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) if isinstance(result, Message): self._insert_callback_data(result) @@ -304,6 +306,7 @@ def copy_message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> MessageId: # We override this method to call self._replace_keyboard return super().copy_message( @@ -320,6 +323,7 @@ def copy_message( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def get_chat( diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index cdf0e53029c..3d9abf0ce64 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1264,6 +1264,36 @@ def filter(self, message: Message) -> bool: web_app_data = _WebAppData() """Messages that contain :attr:`telegram.Message.web_app_data`.""" + class _ForumTopicCreated(MessageFilter): + __slots__ = () + name = 'Filters.status_update.forum_topic_created' + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_created) + + forum_topic_created = _ForumTopicCreated() + """Messages that contain :attr:`telegram.Message.forum_topic_created`.""" + + class _ForumTopicClosed(MessageFilter): + __slots__ = () + name = 'Filters.status_update.forum_topic_closed' + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_closed) + + forum_topic_closed = _ForumTopicClosed() + """Messages that contain :attr:`telegram.Message.forum_topic_closed`.""" + + class _ForumTopicReopened(MessageFilter): + __slots__ = () + name = 'Filters.status_update.forum_topic_reopened' + + def filter(self, message: Message) -> bool: + return bool(message.forum_topic_reopened) + + forum_topic_reopened = _ForumTopicReopened() + """Messages that contain :attr:`telegram.Message.forum_topic_reopened`.""" + name = 'Filters.status_update' def filter(self, message: Update) -> bool: @@ -1288,6 +1318,9 @@ def filter(self, message: Update) -> bool: or self.video_chat_ended(message) or self.video_chat_participants_invited(message) or self.web_app_data(message) + or self.forum_topic_created(message) + or self.forum_topic_closed(message) + or self.forum_topic_reopened(message) ) status_update = _StatusUpdate() @@ -1361,6 +1394,22 @@ def filter(self, message: Update) -> bool: :attr:`telegram.Message.video_chat_participants_invited`. .. versionadded:: 13.12 + web_app_data: Messages that contain + :attr:`telegram.Message.web_app_data`. + + .. versionadded:: 13.12 + forum_topic_created: Messages that contain + :attr:`telegram.Message.forum_topic_created`. + + .. versionadded:: 13.15 + forum_topic_closed: Messages that contain + :attr:`telegram.Message.forum_topic_closed`. + + .. versionadded:: 13.15 + forum_topic_reopened: Messages that contain + :attr:`telegram.Message.forum_topic_reopened`. + + .. versionadded:: 13.15 """ @@ -2212,6 +2261,19 @@ def filter(self, message: Message) -> bool: .. versionadded:: 13.9 """ + class _IsTopicMessage(MessageFilter): + __slots__ = () + name = 'Filters.is_topic_message' + + def filter(self, message: Message) -> bool: + return bool(message.is_topic_message) + + is_topic_message = _IsTopicMessage() + """Messages that contain :attr:`telegram.Message.is_topic_message`. + + .. versionadded:: 13.15 + """ + class _HasProtectedContent(MessageFilter): __slots__ = () name = 'Filters.has_protected_content' diff --git a/telegram/forumtopic.py b/telegram/forumtopic.py new file mode 100644 index 00000000000..a31df35b280 --- /dev/null +++ b/telegram/forumtopic.py @@ -0,0 +1,134 @@ +#!/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 typing import Any + + +from telegram import TelegramObject + + +class ForumTopic(TelegramObject): + """ + 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` + are equal. + + .. versionadded:: 13.15 + + 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, + **_kwargs: Any, + ): + 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:: 13.15 + + 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", "_id_attrs") + + def __init__( + self, + name: str, + icon_color: int, + icon_custom_emoji_id: str = None, + **_kwargs: Any, + ): + 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:: 13.15 + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): # skipcq: PTC-W0049 + pass + + +class ForumTopicReopened(TelegramObject): + """ + This object represents a service message about a forum topic reopened in the chat. + Currently holds no information. + + .. versionadded:: 13.15 + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): # skipcq: PTC-W0049 + pass diff --git a/telegram/message.py b/telegram/message.py index 951b16a9dd0..6af03e93776 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -60,6 +60,7 @@ WebAppData, VideoChatScheduled, ) +from telegram.forumtopic import ForumTopicClosed, ForumTopicCreated, ForumTopicReopened from telegram.utils.helpers import ( escape_markdown, from_timestamp, @@ -263,6 +264,26 @@ class Message(TelegramObject): .. versionadded:: 13.12 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): :obj:`True`, if the message is sent to a forum + topic. + + .. versionadded:: 13.15 + message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 13.15 + forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message: + forum topic created + + .. versionadded:: 13.15 + forum_topic_closed (:class:`telegram.ForumTopicClosed`, optional): Service message: + forum topic closed + + .. versionadded:: 13.15 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`, optional): Service message: + forum topic reopened + + .. versionadded:: 13.15 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. Attributes: @@ -405,6 +426,26 @@ class Message(TelegramObject): .. versionadded:: 13.12 reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. + is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a forum + topic. + + .. versionadded:: 13.15 + message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 13.15 + forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message: + forum topic created + + .. versionadded:: 13.15 + forum_topic_closed (:class:`telegram.ForumTopicClosed`): Optional. Service message: + forum topic closed + + .. versionadded:: 13.15 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`): Optional. Service message: + forum topic reopened + + .. versionadded:: 13.15 bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. .. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored @@ -478,6 +519,11 @@ class Message(TelegramObject): 'is_automatic_forward', 'has_protected_content', 'web_app_data', + 'is_topic_message', + 'message_thread_id', + 'forum_topic_created', + 'forum_topic_closed', + 'forum_topic_reopened', '_id_attrs', ) @@ -587,6 +633,11 @@ def __init__( video_chat_ended: VideoChatEnded = None, video_chat_participants_invited: VideoChatParticipantsInvited = None, 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, **_kwargs: Any, ): if ( @@ -690,7 +741,11 @@ def __init__( self.voice_chat_participants_invited = temp3 self.video_chat_participants_invited = temp3 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.bot = bot self._effective_attachment = DEFAULT_NONE @@ -781,6 +836,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 cls(bot=bot, **data) @@ -893,6 +955,7 @@ def reply_text( entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -924,6 +987,7 @@ def reply_text( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_markdown( @@ -939,6 +1003,7 @@ def reply_markdown( entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -980,6 +1045,7 @@ def reply_markdown( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_markdown_v2( @@ -995,6 +1061,7 @@ def reply_markdown_v2( entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1032,6 +1099,7 @@ def reply_markdown_v2( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_html( @@ -1047,6 +1115,7 @@ def reply_html( entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1084,6 +1153,7 @@ def reply_html( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_media_group( @@ -1098,6 +1168,7 @@ def reply_media_group( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> List['Message']: """Shortcut for:: @@ -1127,6 +1198,7 @@ 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, ) def reply_photo( @@ -1144,6 +1216,7 @@ def reply_photo( filename: str = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1176,6 +1249,7 @@ def reply_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_audio( @@ -1197,6 +1271,7 @@ def reply_audio( filename: str = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1233,6 +1308,7 @@ def reply_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_document( @@ -1252,6 +1328,7 @@ def reply_document( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1286,6 +1363,7 @@ 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, ) def reply_animation( @@ -1307,6 +1385,7 @@ def reply_animation( filename: str = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1343,6 +1422,7 @@ def reply_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_sticker( @@ -1356,6 +1436,7 @@ def reply_sticker( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1384,6 +1465,7 @@ 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, ) def reply_video( @@ -1406,6 +1488,7 @@ def reply_video( filename: str = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1443,6 +1526,7 @@ def reply_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_video_note( @@ -1460,6 +1544,7 @@ def reply_video_note( filename: str = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1492,6 +1577,7 @@ def reply_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_voice( @@ -1510,6 +1596,7 @@ def reply_voice( filename: str = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1543,6 +1630,7 @@ def reply_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_location( @@ -1562,6 +1650,7 @@ def reply_location( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1596,6 +1685,7 @@ 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, ) def reply_venue( @@ -1617,6 +1707,7 @@ def reply_venue( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1653,6 +1744,7 @@ 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, ) def reply_contact( @@ -1670,6 +1762,7 @@ def reply_contact( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1702,6 +1795,7 @@ 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, ) def reply_poll( @@ -1726,6 +1820,7 @@ def reply_poll( explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1765,6 +1860,7 @@ 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, ) def reply_dice( @@ -1778,6 +1874,7 @@ def reply_dice( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1806,6 +1903,7 @@ 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, ) def reply_chat_action( @@ -1844,6 +1942,7 @@ def reply_game( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1874,6 +1973,7 @@ 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, ) def reply_invoice( @@ -1907,6 +2007,7 @@ def reply_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1965,6 +2066,7 @@ 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, ) def forward( @@ -1974,6 +2076,7 @@ def forward( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -2005,6 +2108,7 @@ def forward( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def copy( @@ -2020,6 +2124,7 @@ def copy( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'MessageId': """Shortcut for:: @@ -2049,6 +2154,7 @@ def copy( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def reply_copy( @@ -2066,6 +2172,7 @@ def reply_copy( api_kwargs: JSONDict = None, quote: bool = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'MessageId': """Shortcut for:: @@ -2104,6 +2211,7 @@ def reply_copy( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def edit_text( @@ -2517,6 +2625,145 @@ def unpin( api_kwargs=api_kwargs, ) + def edit_forum_topic( + self, + name: str, + icon_custom_emoji_id: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.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, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def close_forum_topic( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.close_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def reopen_forum_topic( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.reopen_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def delete_forum_topic( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.delete_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def unpin_all_forum_topic_messages( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + 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:: 13.15 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return self.bot.unpin_all_forum_topic_messages( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + timeout=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/user.py b/telegram/user.py index 3825fe09a51..bc1faed3993 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -360,6 +360,7 @@ def send_message( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -384,6 +385,7 @@ def send_message( allow_sending_without_reply=allow_sending_without_reply, entities=entities, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_photo( @@ -400,6 +402,7 @@ def send_photo( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -425,6 +428,7 @@ def send_photo( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_media_group( @@ -438,6 +442,7 @@ def send_media_group( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> List['Message']: """Shortcut for:: @@ -458,6 +463,7 @@ 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, ) def send_audio( @@ -478,6 +484,7 @@ def send_audio( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -507,6 +514,7 @@ def send_audio( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_chat_action( @@ -549,6 +557,7 @@ def send_contact( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -574,6 +583,7 @@ 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, ) def send_dice( @@ -586,6 +596,7 @@ def send_dice( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -607,6 +618,7 @@ 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, ) def send_document( @@ -625,6 +637,7 @@ def send_document( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -652,6 +665,7 @@ 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, ) def send_game( @@ -664,6 +678,7 @@ def send_game( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -685,6 +700,7 @@ 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, ) def send_invoice( @@ -717,6 +733,7 @@ def send_invoice( max_tip_amount: int = None, suggested_tip_amounts: List[int] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -766,6 +783,7 @@ 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, ) def send_location( @@ -784,6 +802,7 @@ def send_location( proximity_alert_radius: int = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -811,6 +830,7 @@ 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, ) def send_animation( @@ -831,6 +851,7 @@ def send_animation( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -860,6 +881,7 @@ def send_animation( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_sticker( @@ -872,6 +894,7 @@ def send_sticker( api_kwargs: JSONDict = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -893,6 +916,7 @@ 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, ) def send_video( @@ -914,6 +938,7 @@ def send_video( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -944,6 +969,7 @@ def send_video( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_venue( @@ -964,6 +990,7 @@ def send_venue( google_place_type: str = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -993,6 +1020,7 @@ 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, ) def send_video_note( @@ -1009,6 +1037,7 @@ def send_video_note( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1034,6 +1063,7 @@ def send_video_note( allow_sending_without_reply=allow_sending_without_reply, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_voice( @@ -1051,6 +1081,7 @@ def send_voice( caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, filename: str = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1077,6 +1108,7 @@ def send_voice( caption_entities=caption_entities, filename=filename, protect_content=protect_content, + message_thread_id=message_thread_id, ) def send_poll( @@ -1101,6 +1133,7 @@ def send_poll( allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, explanation_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'Message': """Shortcut for:: @@ -1133,6 +1166,7 @@ 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, ) def send_copy( @@ -1149,6 +1183,7 @@ def send_copy( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'MessageId': """Shortcut for:: @@ -1174,6 +1209,7 @@ def send_copy( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def copy_message( @@ -1190,6 +1226,7 @@ def copy_message( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, protect_content: bool = None, + message_thread_id: int = None, ) -> 'MessageId': """Shortcut for:: @@ -1215,6 +1252,7 @@ def copy_message( timeout=timeout, api_kwargs=api_kwargs, protect_content=protect_content, + message_thread_id=message_thread_id, ) def approve_join_request( diff --git a/tests/bots.py b/tests/bots.py index 5c8f0fbed2c..34c7622c76f 100644 --- a/tests/bots.py +++ b/tests/bots.py @@ -29,15 +29,16 @@ # These bots are only able to talk in our test chats, so they are quite useless for other # purposes than testing. FALLBACKS = ( - 'W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc' - 'HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj' - 'IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3J' - 'hbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRi' - 'X2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4MTk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1O' - 'URNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsIC' - 'JjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJjaGFubmVsX2l' - 'kIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgImJv' - 'dF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d' + "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" + "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj" + "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 aa9be029f98..12a00784b4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,6 +138,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'] diff --git a/tests/test_bot.py b/tests/test_bot.py index 3706983456f..302f8bcae07 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1851,6 +1851,7 @@ def test_promote_chat_member(self, bot, channel_id, monkeypatch): can_promote_members=True, can_manage_chat=True, can_manage_voice_chats=True, + can_manage_topics=True, ) with pytest.raises( @@ -1892,6 +1893,7 @@ 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) @@ -1909,6 +1911,7 @@ def make_assertion(*args, **_): can_promote_members=9, can_manage_chat=10, can_manage_voice_chats=11, + can_manage_topics=12, ) # Test that video_chats also works @@ -2172,6 +2175,9 @@ 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. + def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout @@ -2307,6 +2313,7 @@ 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 def test_get_set_chat_menu_button(self, bot, chat_id): # Test our chat menu button is commands- @@ -2440,6 +2447,7 @@ def post(url, data, timeout): assert data["disable_notification"] is True assert data["caption_entities"] == [MessageEntity(MessageEntity.BOLD, 0, 4)] assert data['protect_content'] is True + assert data["message_thread_id"] == 1 return data monkeypatch.setattr(bot.request, 'post', post) @@ -2454,6 +2462,7 @@ def post(url, data, timeout): reply_markup=keyboard.to_json() if json_keyboard else keyboard, disable_notification=True, protect_content=True, + message_thread_id=1, ) @flaky(3, 1) diff --git a/tests/test_chat.py b/tests/test_chat.py index bbceb6bb799..94388208ed2 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -46,6 +46,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, ) @@ -72,6 +75,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, recwarn, mro_slots): for attr in chat.__slots__: @@ -103,6 +109,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) @@ -128,6 +137,9 @@ def test_de_json(self, bot): chat.has_restricted_voice_and_video_messages == self.has_restricted_voice_and_video_messages ) + 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() @@ -152,6 +164,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_link(self, chat): assert chat.link == f'https://t.me/{chat.username}' @@ -795,6 +810,128 @@ def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.bot, 'decline_chat_join_request', make_assertion) assert chat.decline_join_request(user_id=42) + def test_create_forum_topic(self, monkeypatch, chat): + 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 check_shortcut_call( + chat.create_forum_topic, + chat.bot, + "create_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert check_defaults_handling(chat.create_forum_topic, chat.bot) + + monkeypatch.setattr(chat.bot, "create_forum_topic", make_assertion) + assert chat.create_forum_topic( + name="New Name", icon_color=0x6FB9F0, icon_custom_emoji_id="12345" + ) + + def test_edit_forum_topic(self, monkeypatch, chat): + 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 check_shortcut_call( + chat.edit_forum_topic, chat.bot, "edit_forum_topic", shortcut_kwargs=["chat_id"] + ) + assert check_defaults_handling(chat.edit_forum_topic, chat.bot) + + monkeypatch.setattr(chat.bot, "edit_forum_topic", make_assertion) + assert chat.edit_forum_topic( + message_thread_id=42, name="New Name", icon_custom_emoji_id="12345" + ) + + def test_close_forum_topic(self, monkeypatch, chat): + 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 check_shortcut_call( + chat.close_forum_topic, + chat.bot, + "close_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert check_defaults_handling(chat.close_forum_topic, chat.bot) + + monkeypatch.setattr(chat.bot, "close_forum_topic", make_assertion) + assert chat.close_forum_topic(message_thread_id=42) + + def test_reopen_forum_topic(self, monkeypatch, chat): + 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 check_shortcut_call( + chat.reopen_forum_topic, + chat.bot, + "reopen_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert check_defaults_handling(chat.reopen_forum_topic, chat.bot) + + monkeypatch.setattr(chat.bot, "reopen_forum_topic", make_assertion) + assert chat.reopen_forum_topic(message_thread_id=42) + + def test_delete_forum_topic(self, monkeypatch, chat): + 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 check_shortcut_call( + chat.delete_forum_topic, + chat.bot, + "delete_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert check_defaults_handling(chat.delete_forum_topic, chat.bot) + + monkeypatch.setattr(chat.bot, "delete_forum_topic", make_assertion) + assert chat.delete_forum_topic(message_thread_id=42) + + def test_unpin_all_forum_topic_messages(self, monkeypatch, chat): + 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 check_shortcut_call( + chat.unpin_all_forum_topic_messages, + chat.bot, + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id"], + ) + assert check_defaults_handling(chat.unpin_all_forum_topic_messages, chat.bot) + + monkeypatch.setattr(chat.bot, "unpin_all_forum_topic_messages", make_assertion) + assert chat.unpin_all_forum_topic_messages(message_thread_id=42) + def test_equality(self): a = Chat(self.id_, self.title, self.type_) b = Chat(self.id_, self.title, self.type_) diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py index defa3bdce99..d913f95fbc7 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) @@ -79,13 +81,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) @@ -101,7 +104,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) @@ -116,7 +119,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 30f4d030519..b56a0150604 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -120,6 +120,7 @@ def test_de_json_all_args(self, bot, chat_member_class_and_status, user): 'can_add_web_page_previews': False, 'can_manage_chat': True, 'can_manage_voice_chats': True, + 'can_manage_topics': True, } chat_member_type = ChatMember.de_json(json_dict, bot) @@ -221,6 +222,7 @@ def test_de_json_subclass(self, chat_member_class_and_status, bot, chat_id, user 'can_add_web_page_previews': False, 'can_manage_chat': True, 'can_manage_video_chats': True, + 'can_manage_topics': True, } assert type(cls.de_json(json_dict, bot)) is cls diff --git a/tests/test_filters.py b/tests/test_filters.py index 9372eb18d38..9509d1d70ab 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -969,6 +969,21 @@ def test_filters_status_update(self, update): assert Filters.status_update.web_app_data(update) update.message.web_app_data = None + update.message.forum_topic_created = "topic" + assert Filters.status_update(update) + assert Filters.status_update.forum_topic_created(update) + update.message.forum_topic_created = None + + update.message.forum_topic_closed = "topic" + assert Filters.status_update(update) + assert Filters.status_update.forum_topic_closed(update) + update.message.forum_topic_closed = None + + update.message.forum_topic_reopened = "topic" + assert Filters.status_update(update) + assert Filters.status_update.forum_topic_reopened(update) + update.message.forum_topic_reopened = None + def test_filters_forwarded(self, update): assert not Filters.forwarded(update) update.message.forward_date = datetime.datetime.utcnow() @@ -1762,6 +1777,11 @@ def test_filters_is_automatic_forward(self, update): update.message.is_automatic_forward = True assert Filters.is_automatic_forward(update) + def test_filters_is_topic_message(self, update): + assert not Filters.is_topic_message(update) + update.message.is_topic_message = True + assert Filters.is_topic_message(update) + def test_filters_has_protected_content(self, update): assert not Filters.has_protected_content(update) update.message.has_protected_content = True diff --git a/tests/test_forum.py b/tests/test_forum.py new file mode 100644 index 00000000000..5aabf7ffe52 --- /dev/null +++ b/tests/test_forum.py @@ -0,0 +1,331 @@ +#!/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") +def emoji_id(bot): + emoji_sticker_list = bot.get_forum_topic_icon_stickers() + first_sticker = emoji_sticker_list[0] + return first_sticker.custom_emoji_id + + +@pytest.fixture +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 +def real_topic(bot, emoji_id, forum_group_id): + result = 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 = 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" + + 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.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) + def test_create_forum_topic(self, real_topic): + 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) + + def test_create_forum_topic_with_only_required_args(self, bot, forum_group_id): + result = 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 = 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) + def test_get_forum_topic_icon_stickers(self, bot): + emoji_sticker_list = 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 + + def test_edit_forum_topic(self, emoji_id, forum_group_id, bot, real_topic): + result = 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) + def test_send_message_to_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + message = 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 + + def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + result = 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 = bot.reopen_forum_topic( + chat_id=forum_group_id, + message_thread_id=message_thread_id, + ) + assert result is True, "Failed to reopen forum topic" + + @pytest.mark.xfail(reason="Can fail due to race conditions in GH actions CI") + def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + msgs = [ + ( + 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 = 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.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 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 isinstance(action, ForumTopicReopened) + + def test_to_dict(self): + action = ForumTopicReopened() + action_dict = action.to_dict() + assert action_dict == {} diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py index d3d0715fa69..b20154d9fde 100644 --- a/tests/test_inputmedia.py +++ b/tests/test_inputmedia.py @@ -43,6 +43,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_file, photo, thumb # noqa: F401 @@ -458,6 +461,19 @@ def test_send_media_group_photo(self, bot, chat_id, media_group): mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages ) + def test_send_media_group_with_message_thread_id( + self, bot, real_topic, forum_group_id, media_group # noqa: F811 + ): + messages = 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) + @flaky(3, 1) def test_send_media_group_all_args(self, bot, chat_id, media_group): m1 = bot.send_message(chat_id, text="test") diff --git a/tests/test_message.py b/tests/test_message.py index 40ca7c79dd7..173d7ad31ca 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -196,6 +196,7 @@ def message(bot): ) }, {'web_app_data': WebAppData('some_data', 'some_button_text')}, + {"message_thread_id": 123}, ], ids=[ 'forwarded_user', @@ -252,6 +253,7 @@ def message(bot): 'is_automatic_forward', 'has_protected_content', 'web_app_data', + 'message_thread_id', ], ) def message_params(bot, request): @@ -1634,6 +1636,122 @@ def test_default_quote(self, message): finally: message.bot.defaults = None + def test_edit_forum_topic(self, monkeypatch, message): + 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 check_shortcut_call( + message.edit_forum_topic, + message.bot, + "edit_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert check_defaults_handling(message.edit_forum_topic, message.bot) + + monkeypatch.setattr(message.bot, "edit_forum_topic", make_assertion) + assert message.edit_forum_topic(name="New Name", icon_custom_emoji_id="12345") + + def test_close_forum_topic(self, monkeypatch, message): + 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 check_shortcut_call( + message.close_forum_topic, + message.bot, + "close_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert check_defaults_handling(message.close_forum_topic, message.bot) + + monkeypatch.setattr(message.bot, "close_forum_topic", make_assertion) + assert message.close_forum_topic() + + def test_reopen_forum_topic(self, monkeypatch, message): + 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 check_shortcut_call( + message.reopen_forum_topic, + message.bot, + "reopen_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert check_defaults_handling(message.reopen_forum_topic, message.bot) + + monkeypatch.setattr(message.bot, "reopen_forum_topic", make_assertion) + assert message.reopen_forum_topic() + + def test_delete_forum_topic(self, monkeypatch, message): + 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 check_shortcut_call( + message.delete_forum_topic, + message.bot, + "delete_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert check_defaults_handling(message.delete_forum_topic, message.bot) + + monkeypatch.setattr(message.bot, "delete_forum_topic", make_assertion) + assert message.delete_forum_topic() + + def test_unpin_all_forum_topic_messages(self, monkeypatch, message): + 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 check_shortcut_call( + message.unpin_all_forum_topic_messages, + message.bot, + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert check_defaults_handling(message.unpin_all_forum_topic_messages, message.bot) + + monkeypatch.setattr(message.bot, "unpin_all_forum_topic_messages", make_assertion) + assert message.unpin_all_forum_topic_messages() + def test_equality(self): id_ = 1 a = Message( diff --git a/tests/test_official.py b/tests/test_official.py index f17f6e7c88e..239fc347beb 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -174,6 +174,7 @@ def check_object(h4): 'is_anonymous', 'is_member', 'until_date', + 'can_manage_topics', } if name == 'BotCommandScope': ignored |= {'type'} # attributes common to all subclasses